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

/** \file
 * \ingroup animrig
 */

#include "DNA_action_types.h"
#include "DNA_anim_types.h"
#include "DNA_array_utils.hh"
#include "DNA_defaults.h"
#include "DNA_scene_types.h"

#include "BLI_listbase.h"
#include "BLI_map.hh"
#include "BLI_math_base.h"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include "BLI_string_utils.hh"
#include "BLI_utildefines.h"

#include "BKE_action.hh"
#include "BKE_anim_data.hh"
#include "BKE_fcurve.hh"
#include "BKE_lib_id.hh"
#include "BKE_library.hh"
#include "BKE_main.hh"
#include "BKE_nla.hh"
#include "BKE_report.hh"

#include "RNA_access.hh"
#include "RNA_path.hh"
#include "RNA_prototypes.hh"

#include "MEM_guardedalloc.h"

#include "BLT_translation.hh"

#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"

#include "ANIM_action.hh"
#include "ANIM_action_iterators.hh"
#include "ANIM_action_legacy.hh"
#include "ANIM_animdata.hh"
#include "ANIM_fcurve.hh"

#include "CLG_log.h"

#include "action_runtime.hh"

#include <cstdio>
#include <cstring>

static CLG_LogRef LOG = {"anim.action"};

namespace blender::animrig {

namespace {
/**
 * Default identifier for action slots. The first two characters in the identifier indicate the ID
 * type of whatever is animated by it.
 *
 * Since the ID type might not be determined when the slot is created, the prefix starts out at
 * XX (see below). Note that no code should use this XX value; use Slot::has_idtype() instead.
 */
constexpr const char *slot_default_name = "Slot";

/**
 * Slot identifier prefix for untyped slots (i.e. where `Slot::has_idtype()` returns `false`).
 */
constexpr const char *slot_untyped_prefix = "XX";

constexpr const char *layer_default_name = "Layer";

}  // namespace

static animrig::Layer &ActionLayer_alloc()
{
  ActionLayer *layer = DNA_struct_default_alloc(ActionLayer);
  return layer->wrap();
}

/* Copied from source/blender/blenkernel/intern/grease_pencil.cc.
 * Keep an eye on DNA_array_utils.hh; we may want to move these functions in there. */
template<typename T> static void grow_array(T **array, int *num, const int add_num)
{
  BLI_assert(add_num > 0);
  const int new_array_num = *num + add_num;
  T *new_array = MEM_calloc_arrayN<T>(new_array_num, "animrig::action/grow_array");

  blender::uninitialized_relocate_n(*array, *num, new_array);
  MEM_SAFE_FREE(*array);

  *array = new_array;
  *num = new_array_num;
}

template<typename T> static void grow_array_and_append(T **array, int *num, T item)
{
  grow_array(array, num, 1);
  (*array)[*num - 1] = item;
}

template<typename T>
static void grow_array_and_insert(T **array, int *num, const int index, T item)
{
  BLI_assert(index >= 0 && index <= *num);
  const int new_array_num = *num + 1;
  T *new_array = MEM_calloc_arrayN<T>(new_array_num, __func__);

  blender::uninitialized_relocate_n(*array, index, new_array);
  new_array[index] = item;
  blender::uninitialized_relocate_n(*array + index, *num - index, new_array + index + 1);

  MEM_SAFE_FREE(*array);

  *array = new_array;
  *num = new_array_num;
}

template<typename T> static void shrink_array(T **array, int *num, const int shrink_num)
{
  BLI_assert(shrink_num > 0);
  const int new_array_num = *num - shrink_num;
  if (new_array_num == 0) {
    MEM_freeN(*array);
    *array = nullptr;
    *num = 0;
    return;
  }

  T *new_array = MEM_calloc_arrayN<T>(new_array_num, __func__);

  blender::uninitialized_move_n(*array, new_array_num, new_array);
  MEM_freeN(*array);

  *array = new_array;
  *num = new_array_num;
}

template<typename T> static void shrink_array_and_remove(T **array, int *num, const int index)
{
  BLI_assert(index >= 0 && index < *num);
  const int new_array_num = *num - 1;
  T *new_array = MEM_calloc_arrayN<T>(new_array_num, __func__);

  blender::uninitialized_move_n(*array, index, new_array);
  blender::uninitialized_move_n(*array + index + 1, *num - index - 1, new_array + index);
  MEM_freeN(*array);

  *array = new_array;
  *num = new_array_num;
}

/**
 * Same as `shrink_array_and_remove()` above, except instead of shifting all the
 * elements after the removed item over to fill the gap, it just swaps in the last
 * element to where the removed element was.
 */
template<typename T> static void shrink_array_and_swap_remove(T **array, int *num, const int index)
{
  BLI_assert(index >= 0 && index < *num);
  const int new_array_num = *num - 1;
  T *new_array = MEM_calloc_arrayN<T>(new_array_num, __func__);

  blender::uninitialized_move_n(*array, index, new_array);
  if (index < new_array_num) {
    new_array[index] = (*array)[new_array_num];
    blender::uninitialized_move_n(*array + index + 1, *num - index - 2, new_array + index + 1);
  }
  MEM_freeN(*array);

  *array = new_array;
  *num = new_array_num;
}

/**
 * Moves the given (end exclusive) range to index `to`, shifting other items
 * before/after to make room.
 *
 * The range is moved such that the *start* ends up at `to`.
 *
 * `to` *must* be far away enough from the end of the array for the entire range
 * to be moved there without spilling over the end of the array.
 */
template<typename T>
static void array_shift_range(
    T *array, const int num, const int range_start, const int range_end, const int to)
{
  BLI_assert(range_start <= range_end);
  BLI_assert(range_end <= num);
  BLI_assert(to <= num + range_start - range_end);
  UNUSED_VARS_NDEBUG(num);

  if (ELEM(range_start, range_end, to)) {
    return;
  }

  if (to < range_start) {
    T *start = array + to;
    T *mid = array + range_start;
    T *end = array + range_end;
    std::rotate(start, mid, end);
  }
  else {
    T *start = array + range_start;
    T *mid = array + range_end;
    T *end = array + to + range_end - range_start;
    std::rotate(start, mid, end);
  }
}

/* ----- Action implementation ----------- */

bool Action::is_empty() const
{
  /* The check for emptiness has to include the check for an empty `groups` ListBase because of the
   * animation filtering code. With the functions `rearrange_action_channels` and
   * `join_groups_action_temp` the ownership of FCurves is temporarily transferred to the `groups`
   * ListBase leaving `curves` potentially empty. */
  return this->layer_array_num == 0 && this->slot_array_num == 0 &&
         BLI_listbase_is_empty(&this->curves) && BLI_listbase_is_empty(&this->groups);
}
bool Action::is_action_legacy() const
{
  /* This is a valid legacy Action only if there is no layered info. */
  return this->layer_array_num == 0 && this->slot_array_num == 0;
}
bool Action::is_action_layered() const
{
  /* This is a valid layered Action if there is ANY layered info (because that
   * takes precedence) or when there is no legacy info. */
  return this->layer_array_num > 0 || this->slot_array_num > 0 ||
         (BLI_listbase_is_empty(&this->curves) && BLI_listbase_is_empty(&this->groups));
}

blender::Span<const Layer *> Action::layers() const
{
  return blender::Span<const Layer *>{reinterpret_cast<Layer **>(this->layer_array),
                                      this->layer_array_num};
}
blender::Span<Layer *> Action::layers()
{
  return blender::Span<Layer *>{reinterpret_cast<Layer **>(this->layer_array),
                                this->layer_array_num};
}
const Layer *Action::layer(const int64_t index) const
{
  return &this->layer_array[index]->wrap();
}
Layer *Action::layer(const int64_t index)
{
  return &this->layer_array[index]->wrap();
}

Layer &Action::layer_add(const std::optional<StringRefNull> name)
{
  Layer &new_layer = ActionLayer_alloc();
  if (name.has_value()) {
    STRNCPY_UTF8(new_layer.name, name.value().c_str());
  }
  else {
    STRNCPY_UTF8(new_layer.name, DATA_(layer_default_name));
  }

  grow_array_and_append<::ActionLayer *>(&this->layer_array, &this->layer_array_num, &new_layer);
  this->layer_active_index = this->layer_array_num - 1;

  /* If this is the first layer in this Action, it means that it could have been
   * used as a legacy Action before. As a result, this->idroot may be non-zero
   * while it should be zero for layered Actions.
   *
   * And since setting this to 0 when it is already supposed to be 0 is fine,
   * there is no check for whether this is actually the first layer. */
  this->idroot = 0;

  return new_layer;
}

static void layer_ptr_destructor(ActionLayer **dna_layer_ptr)
{
  Layer &layer = (*dna_layer_ptr)->wrap();
  MEM_delete(&layer);
};

bool Action::layer_remove(Layer &layer_to_remove)
{
  const int64_t layer_index = this->find_layer_index(layer_to_remove);
  if (layer_index < 0) {
    return false;
  }

  dna::array::remove_index(&this->layer_array,
                           &this->layer_array_num,
                           &this->layer_active_index,
                           layer_index,
                           layer_ptr_destructor);
  return true;
}

void Action::layer_keystrip_ensure()
{
  /* Ensure a layer. */
  Layer *layer;
  if (this->layers().is_empty()) {
    layer = &this->layer_add(DATA_(layer_default_name));
  }
  else {
    layer = this->layer(0);
  }

  /* Ensure a keyframe Strip. */
  if (layer->strips().is_empty()) {
    layer->strip_add(*this, Strip::Type::Keyframe);
  }

  /* Within the limits of Baklava Phase 1, the above code should not have
   * created more than one layer, or more than one strip on the layer. And if a
   * layer + strip already existed, that must have been a keyframe strip. */
  assert_baklava_phase_1_invariants(*this);
}

int64_t Action::find_layer_index(const Layer &layer) const
{
  for (const int64_t layer_index : this->layers().index_range()) {
    const Layer *visit_layer = this->layer(layer_index);
    if (visit_layer == &layer) {
      return layer_index;
    }
  }
  return -1;
}

int64_t Action::find_slot_index(const Slot &slot) const
{
  for (const int64_t slot_index : this->slots().index_range()) {
    const Slot *visit_slot = this->slot(slot_index);
    if (visit_slot == &slot) {
      return slot_index;
    }
  }
  return -1;
}

blender::Span<const Slot *> Action::slots() const
{
  return blender::Span<Slot *>{reinterpret_cast<Slot **>(this->slot_array), this->slot_array_num};
}
blender::Span<Slot *> Action::slots()
{
  return blender::Span<Slot *>{reinterpret_cast<Slot **>(this->slot_array), this->slot_array_num};
}
const Slot *Action::slot(const int64_t index) const
{
  return &this->slot_array[index]->wrap();
}
Slot *Action::slot(const int64_t index)
{
  return &this->slot_array[index]->wrap();
}

Slot *Action::slot_for_handle(const slot_handle_t handle)
{
  const Slot *slot = const_cast<const Action *>(this)->slot_for_handle(handle);
  return const_cast<Slot *>(slot);
}

const Slot *Action::slot_for_handle(const slot_handle_t handle) const
{
  if (handle == Slot::unassigned) {
    return nullptr;
  }

  /* TODO: implement hash-map lookup. */
  for (const Slot *slot : slots()) {
    if (slot->handle == handle) {
      return slot;
    }
  }
  return nullptr;
}

static void slot_identifier_ensure_unique(Action &action, Slot &slot)
{
  auto check_name_is_used = [&](const StringRef name) -> bool {
    for (const Slot *slot_iter : action.slots()) {
      if (slot_iter == &slot) {
        /* Don't compare against the slot that's being renamed. */
        continue;
      }
      if (slot_iter->identifier == name) {
        return true;
      }
    }
    return false;
  };

  BLI_uniquename_cb(check_name_is_used, "", '.', slot.identifier, sizeof(slot.identifier));
}

void Action::slot_display_name_set(Main &bmain, Slot &slot, StringRefNull new_display_name)
{
  this->slot_display_name_define(slot, new_display_name);
  this->slot_identifier_propagate(bmain, slot);
}

void Action::slot_display_name_define(Slot &slot, StringRefNull new_display_name)
{
  BLI_assert_msg(StringRef(new_display_name).size() >= 1,
                 "Action Slot display names must not be empty");
  BLI_assert_msg(StringRef(slot.identifier).size() >= 2,
                 "Action Slot's existing identifier lacks the two-character type prefix, which "
                 "would make the display name copy meaningless due to early null termination.");

  BLI_strncpy_utf8(slot.identifier + 2, new_display_name.c_str(), ARRAY_SIZE(slot.identifier) - 2);
  slot_identifier_ensure_unique(*this, slot);
}

void Action::slot_idtype_define(Slot &slot, ID_Type idtype)
{
  slot.idtype = idtype;
  slot.identifier_ensure_prefix();
  slot_identifier_ensure_unique(*this, slot);
}

void Action::slot_identifier_set(Main &bmain, Slot &slot, const StringRefNull new_identifier)
{
  /* TODO: maybe this function should only set the 'identifier without prefix' aka the 'display
   * name'. That way only `this->id_type` is responsible for the prefix. I (Sybren) think that's
   * easier to determine when the code is a bit more mature, and we can see what the majority of
   * the calls to this function actually do/need. */

  this->slot_identifier_define(slot, new_identifier);
  this->slot_identifier_propagate(bmain, slot);
}

void Action::slot_identifier_define(Slot &slot, const StringRefNull new_identifier)
{
  BLI_assert_msg(
      StringRef(new_identifier).size() >= Slot::identifier_length_min,
      "Action Slot identifiers must be large enough for a 2-letter ID code + the display name");
  STRNCPY_UTF8(slot.identifier, new_identifier.c_str());
  slot_identifier_ensure_unique(*this, slot);
}

void Action::slot_identifier_propagate(Main &bmain, const Slot &slot)
{
  /* Just loop over all animatable IDs in the main database. */
  ListBase *lb;
  ID *id;
  FOREACH_MAIN_LISTBASE_BEGIN (&bmain, lb) {
    FOREACH_MAIN_LISTBASE_ID_BEGIN (lb, id) {
      if (!id_can_have_animdata(id)) {
        /* This ID type cannot have any animation, so ignore all and continue to
         * the next ID type. */
        break;
      }

      AnimData *adt = BKE_animdata_from_id(id);
      if (!adt || adt->action != this) {
        /* Not animated by this Action. */
        continue;
      }
      if (adt->slot_handle != slot.handle) {
        /* Not animated by this Slot. */
        continue;
      }

      /* Ensure the Slot identifier on the AnimData is correct. */
      STRNCPY_UTF8(adt->last_slot_identifier, slot.identifier);
    }
    FOREACH_MAIN_LISTBASE_ID_END;
  }
  FOREACH_MAIN_LISTBASE_END;
}

Slot *Action::slot_find_by_identifier(const StringRefNull slot_identifier)
{
  for (Slot *slot : slots()) {
    if (STREQ(slot->identifier, slot_identifier.c_str())) {
      return slot;
    }
  }
  return nullptr;
}

Slot &Action::slot_allocate()
{
  Slot &slot = *MEM_new<Slot>(__func__);
  this->last_slot_handle++;
  BLI_assert_msg(this->last_slot_handle > 0, "Action Slot handle overflow");
  slot.handle = this->last_slot_handle;

  /* Set the default flags. These cannot be set via the 'DNA defaults' system,
   * as that would require knowing which bit corresponds with which flag. That's
   * only known to the C++ wrapper code. */
  slot.set_expanded(true);
  return slot;
}

Slot &Action::slot_add()
{
  Slot &slot = this->slot_allocate();

  /* Assign the default name and the 'untyped' identifier prefix. */
  STRNCPY_UTF8(slot.identifier, slot_untyped_prefix);
  BLI_strncpy_utf8(slot.identifier + 2, DATA_(slot_default_name), ARRAY_SIZE(slot.identifier) - 2);

  /* Append the Slot to the Action. */
  grow_array_and_append<::ActionSlot *>(&this->slot_array, &this->slot_array_num, &slot);

  slot_identifier_ensure_unique(*this, slot);

  /* If this is the first slot in this Action, it means that it could have
   * been used as a legacy Action before. As a result, this->idroot may be
   * non-zero while it should be zero for layered Actions.
   *
   * And since setting this to 0 when it is already supposed to be 0 is fine,
   * there is no check for whether this is actually the first layer. */
  this->idroot = 0;

  return slot;
}

Slot &Action::slot_add_for_id_type(const ID_Type idtype)
{
  Slot &slot = this->slot_add();

  slot.idtype = idtype;
  slot.identifier_ensure_prefix();
  BLI_strncpy_utf8(slot.identifier + 2, DATA_(slot_default_name), ARRAY_SIZE(slot.identifier) - 2);
  slot_identifier_ensure_unique(*this, slot);

  /* No need to call anim.slot_identifier_propagate() as nothing will be using
   * this brand new Slot yet. */

  return slot;
}

Slot &Action::slot_add_for_id(const ID &animated_id)
{
  Slot &slot = this->slot_add();
  slot.idtype = GS(animated_id.name);

  /* Determine the identifier for this slot, prioritizing transparent
   * auto-selection when toggling between Actions. That's why the last-used slot
   * identifier is used here, and the ID name only as fallback. */
  const AnimData *adt = BKE_animdata_from_id(&animated_id);
  const StringRefNull last_slot_identifier = adt ? adt->last_slot_identifier : "";

  StringRefNull slot_identifier = last_slot_identifier;
  if (slot_identifier.is_empty()) {
    slot_identifier = animated_id.name;
  }

  this->slot_identifier_define(slot, slot_identifier);
  /* No need to call anim.slot_identifier_propagate() as nothing will be using
   * this brand new Slot yet. */

  /* The last-used slot might have had a different ID type through some quirk (changes to linked
   * data, for example). So better ensure that the identifier prefix is correct on this new slot,
   * instead of relying for 100% on the old one. */
  slot.identifier_ensure_prefix();

  return slot;
}

static void slot_ptr_destructor(ActionSlot **dna_slot_ptr)
{
  Slot &slot = (*dna_slot_ptr)->wrap();
  MEM_delete(&slot);
};

bool Action::slot_remove(Slot &slot_to_remove)
{
  /* Check that this slot belongs to this Action. */
  const int64_t slot_index = this->find_slot_index(slot_to_remove);
  if (slot_index < 0) {
    return false;
  }

  /* Remove the slot's data from each keyframe strip. */
  for (StripKeyframeData *strip_data : this->strip_keyframe_data()) {
    strip_data->slot_data_remove(slot_to_remove.handle);
  }

  /* Don't bother un-assigning this slot from its users. The slot handle will
   * not be reused by a new slot anyway. */

  /* Remove the actual slot. */
  dna::array::remove_index(
      &this->slot_array, &this->slot_array_num, nullptr, slot_index, slot_ptr_destructor);
  return true;
}

void Action::slot_move_to_index(Slot &slot, const int to_slot_index)
{
  BLI_assert(this->slots().index_range().contains(to_slot_index));

  const int from_slot_index = this->slots().first_index_try(&slot);
  BLI_assert_msg(from_slot_index >= 0, "Slot not in this action.");

  array_shift_range(
      this->slot_array, this->slot_array_num, from_slot_index, from_slot_index + 1, to_slot_index);
}

void Action::slot_active_set(const slot_handle_t slot_handle)
{
  for (Slot *slot : slots()) {
    slot->set_active(slot->handle == slot_handle);
  }
}

Slot *Action::slot_active_get()
{
  for (Slot *slot : slots()) {
    if (slot->is_active()) {
      return slot;
    }
  }
  return nullptr;
}

bool Action::is_slot_animated(const slot_handle_t slot_handle) const
{
  if (slot_handle == Slot::unassigned) {
    return false;
  }

  Span<const FCurve *> fcurves = fcurves_for_action_slot(*this, slot_handle);
  return !fcurves.is_empty();
}

int Action::strip_keyframe_data_append(StripKeyframeData *strip_data)
{
  BLI_assert(strip_data != nullptr);

  grow_array_and_append<ActionStripKeyframeData *>(
      &this->strip_keyframe_data_array, &this->strip_keyframe_data_array_num, strip_data);

  return this->strip_keyframe_data_array_num - 1;
}

void Action::strip_keyframe_data_remove_if_unused(const int index)
{
  BLI_assert(index >= 0 && index < this->strip_keyframe_data_array_num);

  /* Make sure the data isn't being used anywhere. */
  for (const Layer *layer : this->layers()) {
    for (const Strip *strip : layer->strips()) {
      if (strip->type() == Strip::Type::Keyframe && strip->data_index == index) {
        return;
      }
    }
  }

  /* Free the item to be removed. */
  MEM_delete<StripKeyframeData>(
      static_cast<StripKeyframeData *>(this->strip_keyframe_data_array[index]));

  /* Remove the item, swapping in the item at the end of the array. */
  shrink_array_and_swap_remove<ActionStripKeyframeData *>(
      &this->strip_keyframe_data_array, &this->strip_keyframe_data_array_num, index);

  /* Update strips that pointed at the swapped-in item.
   *
   * Note that we don't special-case the corner-case where the removed data was
   * at the end of the array, but it ends up not mattering because then
   * `old_index == index`. */
  const int old_index = this->strip_keyframe_data_array_num;
  for (Layer *layer : this->layers()) {
    for (Strip *strip : layer->strips()) {
      if (strip->type() == Strip::Type::Keyframe && strip->data_index == old_index) {
        strip->data_index = index;
      }
    }
  }
}

Span<const StripKeyframeData *> Action::strip_keyframe_data() const
{
  /* The reinterpret cast is needed because `strip_keyframe_data_array` is for
   * pointers to the C type `ActionStripKeyframeData`, but we want the C++
   * wrapper type `StripKeyframeData`. */
  return Span<StripKeyframeData *>{
      reinterpret_cast<StripKeyframeData **>(this->strip_keyframe_data_array),
      this->strip_keyframe_data_array_num};
}
Span<StripKeyframeData *> Action::strip_keyframe_data()
{
  /* The reinterpret cast is needed because `strip_keyframe_data_array` is for
   * pointers to the C type `ActionStripKeyframeData`, but we want the C++
   * wrapper type `StripKeyframeData`. */
  return Span<StripKeyframeData *>{
      reinterpret_cast<StripKeyframeData **>(this->strip_keyframe_data_array),
      this->strip_keyframe_data_array_num};
}

Layer *Action::get_layer_for_keyframing()
{
  assert_baklava_phase_1_invariants(*this);

  if (this->layers().is_empty()) {
    return nullptr;
  }

  return this->layer(0);
}

void Action::slot_identifier_ensure_prefix(Slot &slot)
{
  slot.identifier_ensure_prefix();
  slot_identifier_ensure_unique(*this, slot);
}

void Action::slot_setup_for_id(Slot &slot, const ID &animated_id)
{
  if (!ID_IS_EDITABLE(this) || ID_IS_OVERRIDE_LIBRARY(this)) {
    /* Do not write to linked data. For now, also avoid changing the slot identifier on an
     * override. Actions cannot have library overrides at the moment, and when they do, this should
     * actually get designed. For now, it's better to avoid editing data than editing too much. */
    return;
  }

  if (slot.has_idtype()) {
    BLI_assert(slot.idtype == GS(animated_id.name));
    return;
  }

  slot.idtype = GS(animated_id.name);
  this->slot_identifier_ensure_prefix(slot);
}

bool Action::has_keyframes(const slot_handle_t action_slot_handle) const
{
  if (this->is_action_legacy()) {
    /* Old BKE_action_has_motion(const bAction *act) implementation. */
    LISTBASE_FOREACH (const FCurve *, fcu, &this->curves) {
      if (fcu->totvert) {
        return true;
      }
    }
    return false;
  }

  for (const FCurve *fcu : fcurves_for_action_slot(*this, action_slot_handle)) {
    if (fcu->totvert) {
      return true;
    }
  }
  return false;
}

bool Action::has_single_frame() const
{
  bool found_key = false;
  float found_key_frame = 0.0f;

  for (const FCurve *fcu : legacy::fcurves_all(this)) {
    switch (fcu->totvert) {
      case 0:
        /* No keys, so impossible to come to a conclusion on this curve alone. */
        continue;
      case 1:
        /* Single key, which is the complex case, so handle below. */
        break;
      default:
        /* Multiple keys, so there is animation. */
        return false;
    }

    const float this_key_frame = fcu->bezt != nullptr ? fcu->bezt[0].vec[1][0] :
                                                        fcu->fpt[0].vec[0];
    if (!found_key) {
      found_key = true;
      found_key_frame = this_key_frame;
      continue;
    }

    /* The graph editor rounds to 1/1000th of a frame, so it's not necessary to be really precise
     * with these comparisons. */
    if (!compare_ff(found_key_frame, this_key_frame, 0.001f)) {
      /* This key differs from the already-found key, so this Action represents animation. */
      return false;
    }
  }

  /* There is only a single frame if we found at least one key. */
  return found_key;
}

bool Action::is_cyclic() const
{
  return (this->flag & ACT_FRAME_RANGE) && (this->flag & ACT_CYCLIC);
}

/** Return the frame range of the span of keys. */
static float2 get_frame_range_of_fcurves(Span<const FCurve *> fcurves, bool include_modifiers);

float2 Action::get_frame_range() const
{
  if (this->flag & ACT_FRAME_RANGE) {
    return {this->frame_start, this->frame_end};
  }

  Vector<const FCurve *> all_fcurves = legacy::fcurves_all(this);
  return get_frame_range_of_fcurves(all_fcurves, false);
}

float2 Action::get_frame_range_of_slot(const slot_handle_t slot_handle) const
{
  if (this->flag & ACT_FRAME_RANGE) {
    return {this->frame_start, this->frame_end};
  }

  Vector<const FCurve *> legacy_fcurves;
  Span<const FCurve *> fcurves_to_consider;

  if (this->is_action_layered()) {
    fcurves_to_consider = fcurves_for_action_slot(*this, slot_handle);
  }
  else {
    legacy_fcurves = legacy::fcurves_all(this);
    fcurves_to_consider = legacy_fcurves;
  }

  return get_frame_range_of_fcurves(fcurves_to_consider, false);
}

float2 Action::get_frame_range_of_keys(const bool include_modifiers) const
{
  return get_frame_range_of_fcurves(legacy::fcurves_all(this), include_modifiers);
}

static float2 get_frame_range_of_fcurves(Span<const FCurve *> fcurves,
                                         const bool include_modifiers)
{
  float min = 999999999.0f, max = -999999999.0f;
  bool foundvert = false, foundmod = false;

  for (const FCurve *fcu : fcurves) {
    /* if curve has keyframes, consider them first */
    if (fcu->totvert) {
      float nmin, nmax;

      /* get extents for this curve
       * - no "selected only", since this is often used in the backend
       * - no "minimum length" (we will apply this later), otherwise
       *   single-keyframe curves will increase the overall length by
       *   a phantom frame (#50354)
       */
      BKE_fcurve_calc_range(fcu, &nmin, &nmax, false);

      /* compare to the running tally */
      min = min_ff(min, nmin);
      max = max_ff(max, nmax);

      foundvert = true;
    }

    /* if include_modifiers is enabled, need to consider modifiers too
     * - only really care about the last modifier
     */
    if ((include_modifiers) && (fcu->modifiers.last)) {
      FModifier *fcm = static_cast<FModifier *>(fcu->modifiers.last);

      /* only use the maximum sensible limits of the modifiers if they are more extreme */
      switch (fcm->type) {
        case FMODIFIER_TYPE_LIMITS: /* Limits F-Modifier */
        {
          FMod_Limits *fmd = static_cast<FMod_Limits *>(fcm->data);

          if (fmd->flag & FCM_LIMIT_XMIN) {
            min = min_ff(min, fmd->rect.xmin);
          }
          if (fmd->flag & FCM_LIMIT_XMAX) {
            max = max_ff(max, fmd->rect.xmax);
          }
          break;
        }
        case FMODIFIER_TYPE_CYCLES: /* Cycles F-Modifier */
        {
          FMod_Cycles *fmd = static_cast<FMod_Cycles *>(fcm->data);

          if (fmd->before_mode != FCM_EXTRAPOLATE_NONE) {
            min = MINAFRAMEF;
          }
          if (fmd->after_mode != FCM_EXTRAPOLATE_NONE) {
            max = MAXFRAMEF;
          }
          break;
        }
          /* TODO: function modifier may need some special limits */

        default: /* all other standard modifiers are on the infinite range... */
          min = MINAFRAMEF;
          max = MAXFRAMEF;
          break;
      }

      foundmod = true;
    }
  }

  if (foundvert || foundmod) {
    return float2{max_ff(min, MINAFRAMEF), min_ff(max, MAXFRAMEF)};
  }

  return float2{0.0f, 0.0f};
}

/* ----- ActionLayer implementation ----------- */

Layer *Layer::duplicate_with_shallow_strip_copies(const StringRefNull allocation_name) const
{
  ActionLayer *copy = MEM_callocN<ActionLayer>(allocation_name.c_str());
  *copy = *reinterpret_cast<const ActionLayer *>(this);

  /* Make a shallow copy of the Strips, without copying their data. */
  copy->strip_array = MEM_calloc_arrayN<ActionStrip *>(this->strip_array_num,
                                                       allocation_name.c_str());
  for (int i : this->strips().index_range()) {
    Strip *strip_copy = MEM_new<Strip>(allocation_name.c_str(), *this->strip(i));
    copy->strip_array[i] = strip_copy;
  }

  return &copy->wrap();
}

Layer::~Layer()
{
  for (Strip *strip : this->strips()) {
    MEM_delete(strip);
  }
  MEM_SAFE_FREE(this->strip_array);
  this->strip_array_num = 0;
}

blender::Span<const Strip *> Layer::strips() const
{
  return blender::Span<Strip *>{reinterpret_cast<Strip **>(this->strip_array),
                                this->strip_array_num};
}
blender::Span<Strip *> Layer::strips()
{
  return blender::Span<Strip *>{reinterpret_cast<Strip **>(this->strip_array),
                                this->strip_array_num};
}
const Strip *Layer::strip(const int64_t index) const
{
  return &this->strip_array[index]->wrap();
}
Strip *Layer::strip(const int64_t index)
{
  return &this->strip_array[index]->wrap();
}

Strip &Layer::strip_add(Action &owning_action, const Strip::Type strip_type)
{
  Strip &strip = Strip::create(owning_action, strip_type);

  /* Add the new strip to the strip array. */
  grow_array_and_append<::ActionStrip *>(&this->strip_array, &this->strip_array_num, &strip);

  return strip;
}

static void strip_ptr_destructor(ActionStrip **dna_strip_ptr)
{
  Strip &strip = (*dna_strip_ptr)->wrap();
  MEM_delete(&strip);
};

bool Layer::strip_remove(Action &owning_action, Strip &strip)
{
  const int64_t strip_index = this->find_strip_index(strip);
  if (strip_index < 0) {
    return false;
  }

  const Strip::Type strip_type = strip.type();
  const int data_index = strip.data_index;

  dna::array::remove_index(
      &this->strip_array, &this->strip_array_num, nullptr, strip_index, strip_ptr_destructor);

  /* It's important that we do this *after* removing the strip itself
   * (immediately above), because otherwise the strip will be found as a
   * still-existing user of the strip data and thus the strip data won't be
   * removed even if this strip was the last user. */
  switch (strip_type) {
    case Strip::Type::Keyframe:
      owning_action.strip_keyframe_data_remove_if_unused(data_index);
      break;
  }

  return true;
}

int64_t Layer::find_strip_index(const Strip &strip) const
{
  for (const int64_t strip_index : this->strips().index_range()) {
    const Strip *visit_strip = this->strip(strip_index);
    if (visit_strip == &strip) {
      return strip_index;
    }
  }
  return -1;
}

/* ----- ActionSlot implementation ----------- */

Slot::Slot()
{
  /* Zero-initialize the DNA struct. 'this' is a C++ class, and shouldn't be memset like this. */
  memset(static_cast<ActionSlot *>(this), 0, sizeof(ActionSlot));
  this->runtime = MEM_new<SlotRuntime>(__func__);
}

Slot::Slot(const Slot &other) : ActionSlot(other)
{
  this->runtime = MEM_new<SlotRuntime>(__func__);
}

Slot::~Slot()
{
  MEM_delete(this->runtime);
}

void Slot::blend_read_post()
{
  BLI_assert(!this->runtime);
  this->runtime = MEM_new<SlotRuntime>(__func__);
}

bool Slot::is_suitable_for(const ID &animated_id) const
{
  if (!this->has_idtype()) {
    /* Without specific ID type set, this Slot can animate any ID. */
    return true;
  }

  /* Check that the ID type is compatible with this slot. */
  const int animated_idtype = GS(animated_id.name);
  return this->idtype == animated_idtype;
}

bool Slot::has_idtype() const
{
  return this->idtype != 0;
}

Slot::Flags Slot::flags() const
{
  return static_cast<Slot::Flags>(this->slot_flags);
}
bool Slot::is_expanded() const
{
  return this->slot_flags & uint8_t(Flags::Expanded);
}
void Slot::set_expanded(const bool expanded)
{
  if (expanded) {
    this->slot_flags |= uint8_t(Flags::Expanded);
  }
  else {
    this->slot_flags &= ~uint8_t(Flags::Expanded);
  }
}

bool Slot::is_selected() const
{
  return this->slot_flags & uint8_t(Flags::Selected);
}
void Slot::set_selected(const bool selected)
{
  if (selected) {
    this->slot_flags |= uint8_t(Flags::Selected);
  }
  else {
    this->slot_flags &= ~uint8_t(Flags::Selected);
  }
}

bool Slot::is_active() const
{
  return this->slot_flags & uint8_t(Flags::Active);
}
void Slot::set_active(const bool active)
{
  if (active) {
    this->slot_flags |= uint8_t(Flags::Active);
  }
  else {
    this->slot_flags &= ~uint8_t(Flags::Active);
  }
}

Span<ID *> Slot::users(Main &bmain) const
{
  if (bmain.is_action_slot_to_id_map_dirty) {
    internal::rebuild_slot_user_cache(bmain);
  }
  BLI_assert(this->runtime);
  return this->runtime->users.as_span();
}

Vector<ID *> Slot::runtime_users()
{
  BLI_assert_msg(this->runtime, "Slot::runtime should always be allocated");
  return this->runtime->users;
}

void Slot::users_add(ID &animated_id)
{
  BLI_assert(this->runtime);
  this->runtime->users.append_non_duplicates(&animated_id);
}

void Slot::users_remove(ID &animated_id)
{
  BLI_assert(this->runtime);
  Vector<ID *> &users = this->runtime->users;

  /* Even though users_add() ensures that there are no duplicates, there's still things like
   * pointer swapping etc. that can happen via the foreach-id looping code. That means that the
   * entries in the user map are not 100% under control of the user_add() and user_remove()
   * function, and thus we cannot assume that there are no duplicates. */
  users.remove_if([&](const ID *user) { return user == &animated_id; });
}

void Slot::users_invalidate(Main &bmain)
{
  bmain.is_action_slot_to_id_map_dirty = true;
}

std::string Slot::idtype_string() const
{
  if (!this->has_idtype()) {
    return slot_untyped_prefix;
  }

  char name[3] = {0};
  *reinterpret_cast<short *>(name) = this->idtype;
  return name;
}

StringRef Slot::identifier_prefix() const
{
  StringRef identifier(this->identifier);
  BLI_assert(identifier.size() >= 2);

  return identifier.substr(0, 2);
}

StringRefNull Slot::identifier_without_prefix() const
{
  BLI_assert(StringRef(this->identifier).size() >= identifier_length_min);

  /* Avoid accessing an uninitialized part of the string accidentally. */
  if (this->identifier[0] == '\0' || this->identifier[1] == '\0') {
    return "";
  }
  return this->identifier + 2;
}

void Slot::identifier_ensure_prefix()
{
  BLI_assert(StringRef(this->identifier).size() >= identifier_length_min);

  if (StringRef(this->identifier).size() < 2) {
    /* The code below would overwrite the trailing 0-byte. */
    this->identifier[2] = '\0';
  }

  if (!this->has_idtype()) {
    /* A zero idtype is not going to convert to a two-character string, so we
     * need to explicitly assign the default prefix. */
    this->identifier[0] = slot_untyped_prefix[0];
    this->identifier[1] = slot_untyped_prefix[1];
    return;
  }

  *reinterpret_cast<short *>(this->identifier) = this->idtype;
}

/* ----- Functions  ----------- */

Action &action_add(Main &bmain, const StringRefNull name)
{
  bAction *dna_action = BKE_action_add(&bmain, name.c_str());
  id_us_clear_real(&dna_action->id);
  return dna_action->wrap();
}

bool assign_action(bAction *action, ID &animated_id)
{
  AnimData *adt = BKE_animdata_ensure_id(&animated_id);
  if (!adt) {
    return false;
  }
  return assign_action(action, {animated_id, *adt});
}

bool assign_action(bAction *action, const OwnedAnimData owned_adt)
{
  if (!BKE_animdata_action_editable(&owned_adt.adt)) {
    /* Cannot remove, otherwise things turn to custard. */
    BKE_report(nullptr, RPT_ERROR, "Cannot change action, as it is still being edited in NLA");
    return false;
  }

  return generic_assign_action(owned_adt.owner_id,
                               action,
                               owned_adt.adt.action,
                               owned_adt.adt.slot_handle,
                               owned_adt.adt.last_slot_identifier);
}

bool assign_tmpaction(bAction *action, const OwnedAnimData owned_adt)
{
  return generic_assign_action(owned_adt.owner_id,
                               action,
                               owned_adt.adt.tmpact,
                               owned_adt.adt.tmp_slot_handle,
                               owned_adt.adt.tmp_last_slot_identifier);
}

bool unassign_action(ID &animated_id)
{
  return assign_action(nullptr, animated_id);
}

bool unassign_action(OwnedAnimData owned_adt)
{
  return assign_action(nullptr, owned_adt);
}

Slot *assign_action_ensure_slot_for_keying(Action &action, ID &animated_id)
{
  AnimData *adt = BKE_animdata_from_id(&animated_id);
  Slot *slot;

  /* Find a suitable slot, but be stricter about when to allow searching by name
   * than generic_slot_for_autoassign(...). */
  if (adt && adt->action == &action) {
    /* The slot handle is only valid when this action is already assigned.
     * Otherwise it's meaningless. */
    slot = action.slot_for_handle(adt->slot_handle);

    /* If this Action is already assigned, a search by name is inappropriate, as it might
     * re-assign an intentionally-unassigned slot. */
  }
  else {
    /* In this case a by-name search is ok, so defer to generic_slot_for_autoassign(). */
    slot = generic_slot_for_autoassign(animated_id, action, adt ? adt->last_slot_identifier : "");
  }

  /* As a last resort, if there is only one slot and it has no ID type yet, use that. This is what
   * gets created for the backwards compatibility RNA API, for example to allow
   * `action.fcurves.new()`. Key insertion should use that slot as well. */
  if (!slot && action.slots().size() == 1) {
    Slot *first_slot = action.slot(0);
    if (!first_slot->has_idtype()) {
      slot = first_slot;
    }
  }

  /* If no suitable slot was found, create a new one. */
  if (!slot || !slot->is_suitable_for(animated_id)) {
    slot = &action.slot_add_for_id(animated_id);
  }

  /* Only try to assign the Action to the ID if it is not already assigned.
   * Assignment can fail when the ID is in NLA Tweak mode. */
  const bool is_correct_action = adt && adt->action == &action;
  if (!is_correct_action && !assign_action(&action, animated_id)) {
    return nullptr;
  }

  const bool is_correct_slot = adt && adt->slot_handle == slot->handle;
  if (!is_correct_slot && assign_action_slot(slot, animated_id) != ActionSlotAssignmentResult::OK)
  {
    /* This should never happen, as a few lines above a new slot is created for
     * this ID if the found one wasn't deemed suitable. */
    BLI_assert_unreachable();
    return nullptr;
  }

  return slot;
}

static bool is_id_using_action_slot(const ID &animated_id,
                                    const Action &action,
                                    const slot_handle_t slot_handle)
{
  auto visit_action_use = [&](const Action &used_action, slot_handle_t used_slot_handle) -> bool {
    const bool is_used = (&used_action == &action && used_slot_handle == slot_handle);
    return !is_used; /* Stop searching when we found a use of this Action+Slot. */
  };

  const bool looped_until_end = foreach_action_slot_use(animated_id, visit_action_use);
  return !looped_until_end;
}

bool generic_assign_action(ID &animated_id,
                           bAction *action_to_assign,
                           bAction *&action_ptr_ref,
                           slot_handle_t &slot_handle_ref,
                           char *slot_identifier)
{
  BLI_assert(slot_identifier);

  if (action_to_assign && legacy::action_treat_as_legacy(*action_to_assign)) {
    /* Check that the Action is suitable for this ID type.
     * This is only necessary for legacy Actions. */
    if (!BKE_animdata_action_ensure_idroot(&animated_id, action_to_assign)) {
      BKE_reportf(
          nullptr,
          RPT_ERROR,
          "Could not set action '%s' to animate ID '%s', as it does not have suitably rooted "
          "paths for this purpose",
          action_to_assign->id.name + 2,
          animated_id.name);
      return false;
    }
  }

  /* Un-assign any previously-assigned Action first. */
  if (action_ptr_ref) {
    /* Un-assign the slot. This will always succeed, so no need to check the result. */
    if (slot_handle_ref != Slot::unassigned) {
      const ActionSlotAssignmentResult result = generic_assign_action_slot(
          nullptr, animated_id, action_ptr_ref, slot_handle_ref, slot_identifier);
      BLI_assert(result == ActionSlotAssignmentResult::OK);
      UNUSED_VARS_NDEBUG(result);
    }

    /* Un-assign the Action itself. */
    id_us_min(&action_ptr_ref->id);
    action_ptr_ref = nullptr;
  }

  if (!action_to_assign) {
    /* Un-assigning was the point, so the work is done. */
    return true;
  }

  /* Assign the new Action. */
  action_ptr_ref = action_to_assign;
  id_us_plus(&action_ptr_ref->id);

  /* Auto-assign a slot. */
  Slot *slot = generic_slot_for_autoassign(animated_id, action_ptr_ref->wrap(), slot_identifier);
  const ActionSlotAssignmentResult result = generic_assign_action_slot(
      slot, animated_id, action_ptr_ref, slot_handle_ref, slot_identifier);
  BLI_assert(result == ActionSlotAssignmentResult::OK);
  UNUSED_VARS_NDEBUG(result);

  return true;
}

Slot *generic_slot_for_autoassign(const ID &animated_id,
                                  Action &action,
                                  const StringRefNull last_slot_identifier)
{
  /* The slot-finding code in assign_action_ensure_slot_for_keying() is very
   * similar to the code here (differences are documented there). It is very
   * likely that changes in the logic here should be applied there as well. */

  /* Try the slot identifier, if it is set. */
  if (!last_slot_identifier.is_empty()) {
    /* If the last-used slot identifier was 'untyped', i.e. started with XX, see if something more
     * specific to this ID type exists.
     *
     * If there is any choice in the matter, the more specific slot is chosen. In other words, in
     * this case:
     *
     * - last_slot_identifier = `XXSlot`
     * - both `XXSlot` and `OBSlot` exist on the Action (where `OB` represents the ID type of
     *   `animated_id`).
     *
     * the `OBSlot` should be chosen. This means that `XXSlot` NOT being auto-assigned if there is
     * an alternative. Since untyped slots are bound on assignment, this design keeps the Action
     * as-is, which means that the `XXSlot` remains untyped and thus the user is free to assign
     * this to another ID type if desired. */

    const bool last_used_identifier_is_typed = last_slot_identifier.substr(0, 2) !=
                                               slot_untyped_prefix;
    if (!last_used_identifier_is_typed) {
      const std::string with_idtype_prefix = StringRef(animated_id.name, 2) +
                                             last_slot_identifier.substr(2);
      Slot *slot = action.slot_find_by_identifier(with_idtype_prefix);
      if (slot && slot->is_suitable_for(animated_id)) {
        return slot;
      }
    }

    /* See if the actual last-used slot identifier can be matched. */
    Slot *slot = action.slot_find_by_identifier(last_slot_identifier);
    if (slot && slot->is_suitable_for(animated_id)) {
      return slot;
    }

    /* If the last-used slot identifier was IDSomething, and XXSomething exists (where ID = the
     * ID code of the animated ID), fall back to the XX. If slot `IDSomething` existed, the code
     * above would have already returned it. */
    if (last_used_identifier_is_typed) {
      const std::string with_untyped_prefix = StringRef(slot_untyped_prefix) +
                                              last_slot_identifier.substr(2);
      Slot *slot = action.slot_find_by_identifier(with_untyped_prefix);
      if (slot && slot->is_suitable_for(animated_id)) {
        return slot;
      }
    }
  }

  /* Search for the ID name (which includes the ID type). */
  {
    Slot *slot = action.slot_find_by_identifier(animated_id.name);
    if (slot && slot->is_suitable_for(animated_id)) {
      return slot;
    }
  }

  /* If there is only one slot, and it is not specific to any ID type, use that.
   *
   * This should only trigger in some special cases, like legacy Actions that were converted to
   * slotted Actions by the versioning code, where the legacy Action was never assigned to anything
   * (and thus had idroot = 0).
   *
   * This might seem overly specific, and for convenience of automatically auto-assigning a slot,
   * it might be tempting to remove the "slot->has_idtype()" check. However, that would make the
   * following workflow significantly more cumbersome:
   *
   * - Animate `Cube`. This creates `CubeAction` with a single slot `OBCube`.
   * - Assign `CubeAction` to `Suzanne`, with the intent of animating both `Cube` and `Suzanne`
   *   with the same Action.
   * - This should **not** auto-assign the `OBCube` slot to `Suzanne`, as that will overwrite any
   *   property of `Suzanne` with the animated values for the `OBCube` slot.
   *
   * Recovering from this will be hard, as an undo will revert both the overwriting of properties
   * and the assignment of the Action. */
  if (action.slots().size() == 1) {
    Slot *slot = action.slot(0);
    if (!slot->has_idtype()) {
      return slot;
    }
  }

  return nullptr;
}

ActionSlotAssignmentResult generic_assign_action_slot(Slot *slot_to_assign,
                                                      ID &animated_id,
                                                      bAction *&action_ptr_ref,
                                                      slot_handle_t &slot_handle_ref,
                                                      char *slot_identifier)
{
  BLI_assert(slot_identifier);
  if (!action_ptr_ref) {
    /* No action assigned yet, so no way to assign a slot. */
    return ActionSlotAssignmentResult::MissingAction;
  }

  Action &action = action_ptr_ref->wrap();

  /* Check that the slot can actually be assigned. */
  if (slot_to_assign) {
    if (!action.slots().contains(slot_to_assign)) {
      return ActionSlotAssignmentResult::SlotNotFromAction;
    }

    if (!slot_to_assign->is_suitable_for(animated_id)) {
      return ActionSlotAssignmentResult::SlotNotSuitable;
    }
  }

  Slot *slot_to_unassign = action.slot_for_handle(slot_handle_ref);

  /* If there was a previously-assigned slot, unassign it first. */
  slot_handle_ref = Slot::unassigned;
  if (slot_to_unassign) {
    /* Make sure that the stored Slot identifier is up to date. The slot identifier might have
     * changed in a way that wasn't copied into the ADT yet (for example when the
     * Action is linked from another file), so better copy the identifier to be sure
     * that it can be transparently reassigned later.
     *
     * TODO: Replace this with a BLI_assert() that the identifier is as expected, and "simply"
     * ensure this identifier is always correct. */
    BLI_strncpy_utf8(slot_identifier, slot_to_unassign->identifier, Slot::identifier_length_max);

    /* If this was the last use of this slot, remove this ID from its users. */
    if (!is_id_using_action_slot(animated_id, action, slot_to_unassign->handle)) {
      slot_to_unassign->users_remove(animated_id);
    }
  }

  if (!slot_to_assign) {
    return ActionSlotAssignmentResult::OK;
  }

  action.slot_setup_for_id(*slot_to_assign, animated_id);
  slot_handle_ref = slot_to_assign->handle;
  BLI_strncpy_utf8(slot_identifier, slot_to_assign->identifier, Slot::identifier_length_max);
  slot_to_assign->users_add(animated_id);

  return ActionSlotAssignmentResult::OK;
}

ActionSlotAssignmentResult generic_assign_action_slot_handle(slot_handle_t slot_handle_to_assign,
                                                             ID &animated_id,
                                                             bAction *&action_ptr_ref,
                                                             slot_handle_t &slot_handle_ref,
                                                             char *slot_identifier)
{
  if (slot_handle_to_assign == Slot::unassigned && !action_ptr_ref) {
    /* No Action assigned, so no slot was used anyway. Just blindly assign the
     * 'unassigned' handle. */
    slot_handle_ref = Slot::unassigned;
    return ActionSlotAssignmentResult::OK;
  }

  if (!action_ptr_ref) {
    /* No Action to verify the slot handle is valid. As the slot handle will be
     * completely ignored when re-assigning an Action, better to refuse setting
     * it altogether. This will make bugs more obvious. */
    return ActionSlotAssignmentResult::MissingAction;
  }

  Slot *slot = action_ptr_ref->wrap().slot_for_handle(slot_handle_to_assign);
  return generic_assign_action_slot(
      slot, animated_id, action_ptr_ref, slot_handle_ref, slot_identifier);
}

bool is_action_assignable_to(const bAction *dna_action, const ID_Type id_code)
{
  if (!dna_action) {
    /* Clearing the Action is always possible. */
    return true;
  }

  if (dna_action->idroot == 0) {
    /* This is either a never-assigned legacy action, or a layered action. In
     * any case, it can be assigned to any ID. */
    return true;
  }

  const animrig::Action &action = dna_action->wrap();
  if (legacy::action_treat_as_legacy(action)) {
    /* Legacy Actions can only be assigned if their idroot matches. Empty
     * Actions are considered both 'layered' and 'legacy' at the same time,
     * hence this condition checks for 'not layered' rather than 'legacy'. */
    return action.idroot == id_code;
  }

  return true;
}

ActionSlotAssignmentResult assign_action_slot(Slot *slot_to_assign, ID &animated_id)
{
  AnimData *adt = BKE_animdata_from_id(&animated_id);
  if (!adt) {
    return ActionSlotAssignmentResult::MissingAction;
  }

  return generic_assign_action_slot(
      slot_to_assign, animated_id, adt->action, adt->slot_handle, adt->last_slot_identifier);
}

ActionSlotAssignmentResult assign_action_and_slot(Action *action,
                                                  Slot *slot_to_assign,
                                                  ID &animated_id)
{
  if (!assign_action(action, animated_id)) {
    return ActionSlotAssignmentResult::MissingAction;
  }
  return assign_action_slot(slot_to_assign, animated_id);
}

ActionSlotAssignmentResult assign_tmpaction_and_slot_handle(bAction *action,
                                                            const slot_handle_t slot_handle,
                                                            const OwnedAnimData owned_adt)
{
  if (!assign_tmpaction(action, owned_adt)) {
    return ActionSlotAssignmentResult::MissingAction;
  }
  return generic_assign_action_slot_handle(slot_handle,
                                           owned_adt.owner_id,
                                           owned_adt.adt.tmpact,
                                           owned_adt.adt.tmp_slot_handle,
                                           owned_adt.adt.tmp_last_slot_identifier);
}

Action *get_action(ID &animated_id)
{
  AnimData *adt = BKE_animdata_from_id(&animated_id);
  if (!adt) {
    return nullptr;
  }
  if (!adt->action) {
    return nullptr;
  }
  return &adt->action->wrap();
}

std::optional<std::pair<Action *, Slot *>> get_action_slot_pair(ID &animated_id)
{
  AnimData *adt = BKE_animdata_from_id(&animated_id);
  if (!adt || !adt->action) {
    /* Not animated by any Action. */
    return std::nullopt;
  }

  Action &action = adt->action->wrap();
  Slot *slot = action.slot_for_handle(adt->slot_handle);
  if (!slot) {
    /* Will not receive any animation from this Action. */
    return std::nullopt;
  }

  return std::make_pair(&action, slot);
}

/* ----- ActionStrip implementation ----------- */

Strip &Strip::create(Action &owning_action, const Strip::Type type)
{
  /* Create the strip. */
  ActionStrip *strip = MEM_callocN<ActionStrip>(__func__);
  *strip = *DNA_struct_default_get(ActionStrip);
  strip->strip_type = int8_t(type);

  /* Create the strip's data on the owning Action. */
  switch (type) {
    case Strip::Type::Keyframe: {
      StripKeyframeData *strip_data = MEM_new<StripKeyframeData>(__func__);
      strip->data_index = owning_action.strip_keyframe_data_append(strip_data);
      break;
    }
  }

  /* This can happen if someone forgets to add a strip type in the `switch`
   * above, or if someone is evil and passes an invalid strip type to this
   * function. */
  BLI_assert_msg(strip->data_index != -1, "Newly created strip has no data.");

  return strip->wrap();
}

bool Strip::is_infinite() const
{
  return this->frame_start == -std::numeric_limits<float>::infinity() &&
         this->frame_end == std::numeric_limits<float>::infinity();
}

bool Strip::contains_frame(const float frame_time) const
{
  return this->frame_start <= frame_time && frame_time <= this->frame_end;
}

bool Strip::is_last_frame(const float frame_time) const
{
  /* Maybe this needs a more advanced equality check. Implement that when
   * we have an actual example case that breaks. */
  return this->frame_end == frame_time;
}

void Strip::resize(const float frame_start, const float frame_end)
{
  BLI_assert(frame_start <= frame_end);
  BLI_assert_msg(frame_start < std::numeric_limits<float>::infinity(),
                 "only the end frame can be at positive infinity");
  BLI_assert_msg(frame_end > -std::numeric_limits<float>::infinity(),
                 "only the start frame can be at negative infinity");
  this->frame_start = frame_start;
  this->frame_end = frame_end;
}

template<>
const StripKeyframeData &Strip::data<StripKeyframeData>(const Action &owning_action) const
{
  BLI_assert(this->type() == StripKeyframeData::TYPE);

  return *owning_action.strip_keyframe_data()[this->data_index];
}
template<> StripKeyframeData &Strip::data<StripKeyframeData>(Action &owning_action)
{
  BLI_assert(this->type() == StripKeyframeData::TYPE);

  return *owning_action.strip_keyframe_data()[this->data_index];
}

/* ----- ActionStripKeyframeData implementation ----------- */

StripKeyframeData::StripKeyframeData(const StripKeyframeData &other)
    : ActionStripKeyframeData(other)
{
  this->channelbag_array = MEM_calloc_arrayN<ActionChannelbag *>(other.channelbag_array_num,
                                                                 __func__);
  Span<const Channelbag *> channelbags_src = other.channelbags();
  for (int i : channelbags_src.index_range()) {
    this->channelbag_array[i] = MEM_new<animrig::Channelbag>(__func__, *other.channelbag(i));
  }
}

StripKeyframeData::~StripKeyframeData()
{
  for (Channelbag *channelbag_for_slot : this->channelbags()) {
    MEM_delete(channelbag_for_slot);
  }
  MEM_SAFE_FREE(this->channelbag_array);
  this->channelbag_array_num = 0;
}

blender::Span<const Channelbag *> StripKeyframeData::channelbags() const
{
  return blender::Span<Channelbag *>{reinterpret_cast<Channelbag **>(this->channelbag_array),
                                     this->channelbag_array_num};
}
blender::Span<Channelbag *> StripKeyframeData::channelbags()
{
  return blender::Span<Channelbag *>{reinterpret_cast<Channelbag **>(this->channelbag_array),
                                     this->channelbag_array_num};
}
const Channelbag *StripKeyframeData::channelbag(const int64_t index) const
{
  return &this->channelbag_array[index]->wrap();
}
Channelbag *StripKeyframeData::channelbag(const int64_t index)
{
  return &this->channelbag_array[index]->wrap();
}
const Channelbag *StripKeyframeData::channelbag_for_slot(const slot_handle_t slot_handle) const
{
  for (const Channelbag *channels : this->channelbags()) {
    if (channels->slot_handle == slot_handle) {
      return channels;
    }
  }
  return nullptr;
}
int64_t StripKeyframeData::find_channelbag_index(const Channelbag &channelbag) const
{
  for (int64_t index = 0; index < this->channelbag_array_num; index++) {
    if (this->channelbag(index) == &channelbag) {
      return index;
    }
  }
  return -1;
}
Channelbag *StripKeyframeData::channelbag_for_slot(const slot_handle_t slot_handle)
{
  const auto *const_this = const_cast<const StripKeyframeData *>(this);
  const auto *const_channels = const_this->channelbag_for_slot(slot_handle);
  return const_cast<Channelbag *>(const_channels);
}
const Channelbag *StripKeyframeData::channelbag_for_slot(const Slot &slot) const
{
  return this->channelbag_for_slot(slot.handle);
}
Channelbag *StripKeyframeData::channelbag_for_slot(const Slot &slot)
{
  return this->channelbag_for_slot(slot.handle);
}

Channelbag &StripKeyframeData::channelbag_for_slot_add(const Slot &slot)
{
  return this->channelbag_for_slot_add(slot.handle);
}

Channelbag &StripKeyframeData::channelbag_for_slot_add(const slot_handle_t slot_handle)
{
  BLI_assert_msg(channelbag_for_slot(slot_handle) == nullptr,
                 "Cannot add channelbag for already-registered slot");
  BLI_assert_msg(slot_handle != Slot::unassigned, "Cannot add channelbag for 'unassigned' slot");

  Channelbag &channels = MEM_new<ActionChannelbag>(__func__)->wrap();
  channels.slot_handle = slot_handle;

  grow_array_and_append<ActionChannelbag *>(
      &this->channelbag_array, &this->channelbag_array_num, &channels);

  return channels;
}

Channelbag &StripKeyframeData::channelbag_for_slot_ensure(const Slot &slot)
{
  return this->channelbag_for_slot_ensure(slot.handle);
}

Channelbag &StripKeyframeData::channelbag_for_slot_ensure(const slot_handle_t slot_handle)
{
  Channelbag *channelbag = this->channelbag_for_slot(slot_handle);
  if (channelbag != nullptr) {
    return *channelbag;
  }
  return this->channelbag_for_slot_add(slot_handle);
}

static void channelbag_ptr_destructor(ActionChannelbag **dna_channelbag_ptr)
{
  Channelbag &channelbag = (*dna_channelbag_ptr)->wrap();
  MEM_delete(&channelbag);
};

bool StripKeyframeData::channelbag_remove(Channelbag &channelbag_to_remove)
{
  const int64_t channelbag_index = this->find_channelbag_index(channelbag_to_remove);
  if (channelbag_index < 0) {
    return false;
  }

  dna::array::remove_index(&this->channelbag_array,
                           &this->channelbag_array_num,
                           nullptr,
                           channelbag_index,
                           channelbag_ptr_destructor);

  return true;
}

void StripKeyframeData::slot_data_remove(const slot_handle_t slot_handle)
{
  Channelbag *channelbag = this->channelbag_for_slot(slot_handle);
  if (!channelbag) {
    return;
  }
  this->channelbag_remove(*channelbag);
}

void StripKeyframeData::slot_data_duplicate(const slot_handle_t source_slot_handle,
                                            const slot_handle_t target_slot_handle)
{
  BLI_assert(!this->channelbag_for_slot(target_slot_handle));

  const Channelbag *source_cbag = this->channelbag_for_slot(source_slot_handle);
  if (!source_cbag) {
    return;
  }

  Channelbag &target_cbag = *MEM_new<animrig::Channelbag>(__func__, *source_cbag);
  target_cbag.slot_handle = target_slot_handle;

  grow_array_and_append<ActionChannelbag *>(
      &this->channelbag_array, &this->channelbag_array_num, &target_cbag);
}

const FCurve *Channelbag::fcurve_find(const FCurveDescriptor &fcurve_descriptor) const
{
  return animrig::fcurve_find(this->fcurves(), fcurve_descriptor);
}

FCurve *Channelbag::fcurve_find(const FCurveDescriptor &fcurve_descriptor)
{
  /* Intermediate variable needed to disambiguate const/non-const overloads. */
  Span<FCurve *> fcurves = this->fcurves();
  return animrig::fcurve_find(fcurves, fcurve_descriptor);
}

FCurve &Channelbag::fcurve_ensure(Main *bmain, const FCurveDescriptor &fcurve_descriptor)
{
  if (FCurve *existing_fcurve = this->fcurve_find(fcurve_descriptor)) {
    return *existing_fcurve;
  }
  return this->fcurve_create(bmain, fcurve_descriptor);
}

FCurve *Channelbag::fcurve_create_unique(Main *bmain, const FCurveDescriptor &fcurve_descriptor)
{
  if (this->fcurve_find(fcurve_descriptor)) {
    return nullptr;
  }
  return &this->fcurve_create(bmain, fcurve_descriptor);
}

FCurve &Channelbag::fcurve_create(Main *bmain, const FCurveDescriptor &fcurve_descriptor)
{
  FCurve *new_fcurve = create_fcurve_for_channel(fcurve_descriptor);

  if (this->fcurve_array_num == 0) {
    new_fcurve->flag |= FCURVE_ACTIVE; /* First curve is added active. */
  }

  bActionGroup *group = fcurve_descriptor.channel_group.has_value() ?
                            &this->channel_group_ensure(*fcurve_descriptor.channel_group) :
                            nullptr;
  const int insert_index = group ? group->fcurve_range_start + group->fcurve_range_length :
                                   this->fcurve_array_num;
  BLI_assert(insert_index <= this->fcurve_array_num);

  grow_array_and_insert(&this->fcurve_array, &this->fcurve_array_num, insert_index, new_fcurve);
  if (group) {
    group->fcurve_range_length += 1;
    this->restore_channel_group_invariants();
  }

  if (bmain) {
    DEG_relations_tag_update(bmain);
  }

  return *new_fcurve;
}

Vector<FCurve *> Channelbag::fcurve_create_many(Main *bmain,
                                                Span<FCurveDescriptor> fcurve_descriptors)
{
  const int prev_fcurve_num = this->fcurve_array_num;
  const int add_fcurve_num = int(fcurve_descriptors.size());
  const bool make_first_active = prev_fcurve_num == 0;

  /* Figure out which path+index combinations already exist. */
  struct CurvePathIndex {
    StringRefNull rna_path;
    int array_index;
    bool operator==(const CurvePathIndex &o) const
    {
      /* Check indices first, cheaper than a string comparison. */
      return this->array_index == o.array_index && this->rna_path == o.rna_path;
    }
    uint64_t hash() const
    {
      return get_default_hash(this->rna_path, this->array_index);
    }
  };
  Set<CurvePathIndex> unique_curves;
  unique_curves.reserve(prev_fcurve_num);
  for (FCurve *fcurve : this->fcurves()) {
    CurvePathIndex path_index;
    path_index.rna_path = StringRefNull(fcurve->rna_path ? fcurve->rna_path : "");
    path_index.array_index = fcurve->array_index;
    unique_curves.add(path_index);
  }

  /* Grow curves array with enough space for new curves. */
  grow_array(&this->fcurve_array, &this->fcurve_array_num, add_fcurve_num);

  /* Add the new curves. */
  Vector<FCurve *> new_fcurves;
  new_fcurves.resize(add_fcurve_num);
  int curve_index = prev_fcurve_num;
  for (int i = 0; i < add_fcurve_num; i++) {
    const FCurveDescriptor &desc = fcurve_descriptors[i];

    CurvePathIndex path_index;
    path_index.rna_path = desc.rna_path;
    path_index.array_index = desc.array_index;
    if (desc.rna_path.is_empty() || !unique_curves.add(path_index)) {
      /* Empty input path, or such curve already exists. */
      new_fcurves[i] = nullptr;
      continue;
    }

    FCurve *fcurve = create_fcurve_for_channel(desc);
    new_fcurves[i] = fcurve;

    this->fcurve_array[curve_index] = fcurve;
    if (desc.channel_group.has_value()) {
      bActionGroup *group = &this->channel_group_ensure(*desc.channel_group);
      const int insert_index = group->fcurve_range_start + group->fcurve_range_length;
      BLI_assert(insert_index <= this->fcurve_array_num);
      /* Insert curve into proper array place at the end of the group. Note: this can
       * still lead to quadratic complexity, in practice was not found to be an issue yet. */
      array_shift_range(
          this->fcurve_array, this->fcurve_array_num, curve_index, curve_index + 1, insert_index);
      group->fcurve_range_length++;

      /* Update curve start ranges of the following groups. */
      int index = this->channel_group_find_index(group);
      BLI_assert(index >= 0 && index < this->group_array_num);
      for (index = index + 1; index < this->group_array_num; index++) {
        this->group_array[index]->fcurve_range_start++;
      }
    }
    curve_index++;
  }

  if (this->fcurve_array_num != curve_index) {
    /* Some curves were not created, resize to final amount. */
    shrink_array(
        &this->fcurve_array, &this->fcurve_array_num, this->fcurve_array_num - curve_index);
  }

  if (make_first_active) {
    /* Set first created curve as active. */
    for (FCurve *fcurve : new_fcurves) {
      if (fcurve != nullptr) {
        fcurve->flag |= FCURVE_ACTIVE;
        break;
      }
    }
  }

  this->restore_channel_group_invariants();
  if (bmain) {
    DEG_relations_tag_update(bmain);
  }
  return new_fcurves;
}

void Channelbag::fcurve_append(FCurve &fcurve)
{
  /* Appended F-Curves don't belong to any group yet, so better make sure their
   * group pointer reflects that. */
  fcurve.grp = nullptr;

  grow_array_and_append(&this->fcurve_array, &this->fcurve_array_num, &fcurve);
}

static void fcurve_ptr_destructor(FCurve **fcurve_ptr)
{
  BKE_fcurve_free(*fcurve_ptr);
};

bool Channelbag::fcurve_remove(FCurve &fcurve_to_remove)
{
  if (!this->fcurve_detach(fcurve_to_remove)) {
    return false;
  }
  BKE_fcurve_free(&fcurve_to_remove);
  return true;
}

void Channelbag::fcurve_remove_by_index(const int64_t fcurve_index)
{
  /* Grab the pointer before it's detached, so we can free it after. */
  FCurve *fcurve_to_remove = this->fcurve(fcurve_index);

  this->fcurve_detach_by_index(fcurve_index);

  BKE_fcurve_free(fcurve_to_remove);
}

static void fcurve_ptr_noop_destructor(FCurve ** /*fcurve_ptr*/) {}

bool Channelbag::fcurve_detach(FCurve &fcurve_to_detach)
{
  const int64_t fcurve_index = this->fcurves().first_index_try(&fcurve_to_detach);
  if (fcurve_index < 0) {
    return false;
  }
  this->fcurve_detach_by_index(fcurve_index);
  return true;
}

void Channelbag::fcurve_detach_by_index(const int64_t fcurve_index)
{
  BLI_assert(fcurve_index >= 0);
  BLI_assert(fcurve_index < this->fcurve_array_num);

  const int group_index = this->channel_group_containing_index(fcurve_index);
  if (group_index != -1) {
    bActionGroup *group = this->channel_group(group_index);

    group->fcurve_range_length -= 1;
    if (group->fcurve_range_length <= 0) {
      const int group_index = this->channel_groups().first_index_try(group);
      this->channel_group_remove_raw(group_index);
    }
  }

  dna::array::remove_index(&this->fcurve_array,
                           &this->fcurve_array_num,
                           nullptr,
                           fcurve_index,
                           fcurve_ptr_noop_destructor);

  this->restore_channel_group_invariants();

  /* As an optimization, this function could call `DEG_relations_tag_update(bmain)` to prune any
   * relationships that are now no longer necessary. This is not needed for correctness of the
   * depsgraph evaluation results though. */
}

void Channelbag::fcurve_move_to_index(FCurve &fcurve, int to_fcurve_index)
{
  BLI_assert(to_fcurve_index >= 0 && to_fcurve_index < this->fcurves().size());

  const int fcurve_index = this->fcurves().first_index_try(&fcurve);
  BLI_assert_msg(fcurve_index >= 0, "FCurve not in this channel bag.");

  array_shift_range(
      this->fcurve_array, this->fcurve_array_num, fcurve_index, fcurve_index + 1, to_fcurve_index);

  this->restore_channel_group_invariants();
}

void Channelbag::fcurves_clear()
{
  dna::array::clear(&this->fcurve_array, &this->fcurve_array_num, nullptr, fcurve_ptr_destructor);

  /* Since all F-Curves are gone, the groups are all empty. */
  for (bActionGroup *group : channel_groups()) {
    group->fcurve_range_start = 0;
    group->fcurve_range_length = 0;
  }
}

static void cyclic_keying_ensure_modifier(FCurve &fcurve)
{
  /* #BKE_fcurve_get_cycle_type() only looks at the first modifier to see if it's a Cycle modifier,
   * so if we're going to add one, better make sure it's the first one.
   *
   * BUT: #add_fmodifier() only allows adding a Cycle modifier when there are none yet, so that's
   * all that we need to check for here.
   */
  if (!BLI_listbase_is_empty(&fcurve.modifiers)) {
    return;
  }

  add_fmodifier(&fcurve.modifiers, FMODIFIER_TYPE_CYCLES, &fcurve);
}

/**
 * Ensure there are at least two keys in this F-Curve, in order to define A cycle range.
 *
 * That range does NOT have to be the same as `cycle_range` -- that parameter is only used to
 * insert a 2nd key when there is only one.
 *
 * \note This function ONLY does something when there is a single keyframe. If there are more, the
 * first and last keys already define the cycle range. If that range is not the same as the
 * `cycle_range` parameter, it's seen as an animator's choice and won't be adjusted.
 */
static void cyclic_keying_ensure_cycle_range_exists(FCurve &fcurve, const float2 cycle_range)
{
  /* This is basically a copy of the legacy function `make_new_fcurve_cyclic()`
   * in `keyframing.cc`, except that it's limited to only one thing (ensuring
   * two keys exist to make cycling possible). Creating the F-Curve modifier is
   * the responsibility of another function. */

  if (fcurve.totvert != 1 || fcurve.bezt == nullptr) {
    return;
  }

  const float period = cycle_range[1] - cycle_range[0];
  if (period < 0.1f) {
    return;
  }

  /* Move the one existing keyframe into the cycle range. */
  const float frame_offset = fcurve.bezt[0].vec[1][0] - cycle_range[0];
  const float fix = floorf(frame_offset / period) * period;

  fcurve.bezt[0].vec[0][0] -= fix;
  fcurve.bezt[0].vec[1][0] -= fix;
  fcurve.bezt[0].vec[2][0] -= fix;

  /* Reallocate the array to make space for the 2nd point. */
  fcurve.totvert++;
  fcurve.bezt = static_cast<BezTriple *>(
      MEM_reallocN(fcurve.bezt, sizeof(BezTriple) * fcurve.totvert));

  /* Duplicate and offset the keyframe. */
  fcurve.bezt[1] = fcurve.bezt[0];
  fcurve.bezt[1].vec[0][0] += period;
  fcurve.bezt[1].vec[1][0] += period;
  fcurve.bezt[1].vec[2][0] += period;
}

SingleKeyingResult StripKeyframeData::keyframe_insert(Main *bmain,
                                                      const Slot &slot,
                                                      const FCurveDescriptor &fcurve_descriptor,
                                                      const float2 time_value,
                                                      const KeyframeSettings &settings,
                                                      const eInsertKeyFlags insert_key_flags,
                                                      const std::optional<float2> cycle_range)
{
  /* Get the fcurve, or create one if it doesn't exist and the keying flags
   * allow. */
  FCurve *fcurve = nullptr;
  if (key_insertion_may_create_fcurve(insert_key_flags)) {
    fcurve = &this->channelbag_for_slot_ensure(slot).fcurve_ensure(bmain, fcurve_descriptor);
  }
  else {
    Channelbag *channels = this->channelbag_for_slot(slot);
    if (channels != nullptr) {
      fcurve = channels->fcurve_find(fcurve_descriptor);
    }
  }

  if (!fcurve) {
    CLOG_WARN(&LOG,
              "FCurve %s[%d] for slot %s was not created due to either the Only Insert "
              "Available setting or Replace keyframing mode.\n",
              fcurve_descriptor.rna_path.c_str(),
              fcurve_descriptor.array_index,
              slot.identifier);
    return SingleKeyingResult::CANNOT_CREATE_FCURVE;
  }

  if (!BKE_fcurve_is_keyframable(fcurve)) {
    /* TODO: handle this properly, in a way that can be communicated to the user. */
    CLOG_WARN(&LOG,
              "FCurve %s[%d] for slot %s doesn't allow inserting keys.\n",
              fcurve_descriptor.rna_path.c_str(),
              fcurve_descriptor.array_index,
              slot.identifier);
    return SingleKeyingResult::FCURVE_NOT_KEYFRAMEABLE;
  }

  if (cycle_range && (*cycle_range)[0] < (*cycle_range)[1]) {
    /* Cyclic keying consists of three things:
     * - Ensure there is a Cycle modifier on the F-Curve.
     * - Ensure the start and end of the cycle have explicit keys, so that the
     *   cycle modifier knows how to cycle (it doesn't look at the Action, and
     *   as long as the period is correct, the first/last keys don't have to
     *   align with the Action start/end).
     * - Offset the key to insert so that it falls within the cycle range.
     */
    cyclic_keying_ensure_modifier(*fcurve);
    cyclic_keying_ensure_cycle_range_exists(*fcurve, *cycle_range);
    /* Offsetting the key doesn't have to happen here, as insert_vert_fcurve()
     * takes care of that. */
  }

  const SingleKeyingResult insert_vert_result = insert_vert_fcurve(
      fcurve, time_value, settings, insert_key_flags);

  if (insert_vert_result != SingleKeyingResult::SUCCESS) {
    CLOG_WARN(&LOG,
              "Could not insert key into FCurve %s[%d] for slot %s.\n",
              fcurve_descriptor.rna_path.c_str(),
              fcurve_descriptor.array_index,
              slot.identifier);
    return insert_vert_result;
  }

  if (fcurve_descriptor.prop_type) {
    update_autoflags_fcurve_direct(fcurve, *fcurve_descriptor.prop_type);
  }

  return SingleKeyingResult::SUCCESS;
}

/* ActionChannelbag implementation. */

Channelbag::Channelbag(const Channelbag &other)
{
  this->slot_handle = other.slot_handle;

  this->fcurve_array_num = other.fcurve_array_num;
  this->fcurve_array = MEM_calloc_arrayN<FCurve *>(other.fcurve_array_num, __func__);
  for (int i = 0; i < other.fcurve_array_num; i++) {
    const FCurve *fcu_src = other.fcurve_array[i];
    this->fcurve_array[i] = BKE_fcurve_copy(fcu_src);
  }

  this->group_array_num = other.group_array_num;
  this->group_array = MEM_calloc_arrayN<bActionGroup *>(other.group_array_num, __func__);
  for (int i = 0; i < other.group_array_num; i++) {
    const bActionGroup *group_src = other.group_array[i];
    this->group_array[i] = static_cast<bActionGroup *>(MEM_dupallocN(group_src));
    this->group_array[i]->channelbag = this;
  }

  /* BKE_fcurve_copy() resets the FCurve's group pointer. Which is good, because the groups are
   * duplicated too. This sets the group pointers to the correct values. */
  this->restore_channel_group_invariants();
}

Channelbag::~Channelbag()
{
  for (FCurve *fcu : this->fcurves()) {
    BKE_fcurve_free(fcu);
  }
  MEM_SAFE_FREE(this->fcurve_array);
  this->fcurve_array_num = 0;

  for (bActionGroup *group : this->channel_groups()) {
    MEM_SAFE_FREE(group);
  }
  MEM_SAFE_FREE(this->group_array);
  this->group_array_num = 0;
}

blender::Span<const FCurve *> Channelbag::fcurves() const
{
  return blender::Span<FCurve *>{this->fcurve_array, this->fcurve_array_num};
}
blender::Span<FCurve *> Channelbag::fcurves()
{
  return blender::Span<FCurve *>{this->fcurve_array, this->fcurve_array_num};
}
const FCurve *Channelbag::fcurve(const int64_t index) const
{
  return this->fcurve_array[index];
}
FCurve *Channelbag::fcurve(const int64_t index)
{
  return this->fcurve_array[index];
}

blender::Span<const bActionGroup *> Channelbag::channel_groups() const
{
  return blender::Span<bActionGroup *>{this->group_array, this->group_array_num};
}
blender::Span<bActionGroup *> Channelbag::channel_groups()
{
  return blender::Span<bActionGroup *>{this->group_array, this->group_array_num};
}
const bActionGroup *Channelbag::channel_group(const int64_t index) const
{
  BLI_assert(index < this->group_array_num);
  return this->group_array[index];
}
bActionGroup *Channelbag::channel_group(const int64_t index)
{
  BLI_assert(index < this->group_array_num);
  return this->group_array[index];
}

const bActionGroup *Channelbag::channel_group_find(const StringRef name) const
{
  for (const bActionGroup *group : this->channel_groups()) {
    if (name == StringRef{group->name}) {
      return group;
    }
  }

  return nullptr;
}

int Channelbag::channel_group_find_index(const bActionGroup *group) const
{
  for (int i = 0; i < this->group_array_num; i++) {
    if (this->group_array[i] == group) {
      return i;
    }
  }
  return -1;
}

bActionGroup *Channelbag::channel_group_find(const StringRef name)
{
  /* Intermediate variable needed to disambiguate const/non-const overloads. */
  Span<bActionGroup *> groups = this->channel_groups();
  for (bActionGroup *group : groups) {
    if (name == StringRef{group->name}) {
      return group;
    }
  }

  return nullptr;
}

int Channelbag::channel_group_containing_index(const int fcurve_array_index)
{
  int i = 0;
  for (const bActionGroup *group : this->channel_groups()) {
    if (fcurve_array_index >= group->fcurve_range_start &&
        fcurve_array_index < (group->fcurve_range_start + group->fcurve_range_length))
    {
      return i;
    }
    i++;
  }

  return -1;
}

bActionGroup &Channelbag::channel_group_create(StringRefNull name)
{
  bActionGroup *new_group = MEM_callocN<bActionGroup>(__func__);

  /* Find the end fcurve index of the current channel groups, to be used as the
   * start of the new channel group. */
  int fcurve_index = 0;
  const int length = this->channel_groups().size();
  if (length > 0) {
    const bActionGroup *last = this->channel_group(length - 1);
    fcurve_index = last->fcurve_range_start + last->fcurve_range_length;
  }
  new_group->fcurve_range_start = fcurve_index;

  new_group->channelbag = this;

  /* Make it selected. */
  new_group->flag = AGRP_SELECTED;

  /* Ensure it has a unique name.
   *
   * Note that this only happens here (upon creation). The user can later rename
   * groups to have duplicate names. This is stupid, but it's how the legacy
   * system worked, and at the time of writing this code we're just trying to
   * match that system's behavior, even when it's goofy. */
  std::string unique_name = BLI_uniquename_cb(
      [&](const StringRef name) {
        for (const bActionGroup *group : this->channel_groups()) {
          if (STREQ(group->name, name.data())) {
            return true;
          }
        }
        return false;
      },
      '.',
      name[0] == '\0' ? DATA_("Group") : name);

  STRNCPY_UTF8(new_group->name, unique_name.c_str());

  grow_array_and_append(&this->group_array, &this->group_array_num, new_group);

  return *new_group;
}

bActionGroup &Channelbag::channel_group_ensure(StringRefNull name)
{
  bActionGroup *group = this->channel_group_find(name);
  if (group) {
    return *group;
  }

  return this->channel_group_create(name);
}

bool Channelbag::channel_group_remove(bActionGroup &group)
{
  const int group_index = this->channel_groups().first_index_try(&group);
  if (group_index == -1) {
    return false;
  }

  /* Move the group's fcurves to just past the end of where the grouped
   * fcurves will be after this group is removed. */
  const bActionGroup *last_group = this->channel_groups().last();
  BLI_assert(last_group != nullptr);
  const int to_index = last_group->fcurve_range_start + last_group->fcurve_range_length -
                       group.fcurve_range_length;
  array_shift_range(this->fcurve_array,
                    this->fcurve_array_num,
                    group.fcurve_range_start,
                    group.fcurve_range_start + group.fcurve_range_length,
                    to_index);

  this->channel_group_remove_raw(group_index);
  this->restore_channel_group_invariants();

  return true;
}

void Channelbag::channel_group_move_to_index(bActionGroup &group, const int to_group_index)
{
  BLI_assert(to_group_index >= 0 && to_group_index < this->channel_groups().size());

  const int group_index = this->channel_groups().first_index_try(&group);
  BLI_assert_msg(group_index >= 0, "Group not in this channel bag.");

  /* Shallow copy, to track which fcurves should be moved in the second step. */
  const bActionGroup pre_move_group = group;

  /* First we move the group to its new position. The call to
   * `restore_channel_group_invariants()` is necessary to update the group's
   * fcurve range (as well as the ranges of the other groups) to match its new
   * position in the group array. */
  array_shift_range(
      this->group_array, this->group_array_num, group_index, group_index + 1, to_group_index);
  this->restore_channel_group_invariants();

  /* Move the fcurves that were part of `group` (as recorded in
   *`pre_move_group`) to their new positions (now in `group`) so that they're
   * part of `group` again. */
  array_shift_range(this->fcurve_array,
                    this->fcurve_array_num,
                    pre_move_group.fcurve_range_start,
                    pre_move_group.fcurve_range_start + pre_move_group.fcurve_range_length,
                    group.fcurve_range_start);
  this->restore_channel_group_invariants();
}

void Channelbag::channel_group_remove_raw(const int group_index)
{
  BLI_assert(group_index >= 0 && group_index < this->channel_groups().size());

  MEM_SAFE_FREE(this->group_array[group_index]);
  shrink_array_and_remove(&this->group_array, &this->group_array_num, group_index);
}

void Channelbag::restore_channel_group_invariants()
{
  /* Shift channel groups. */
  {
    int start_index = 0;
    for (bActionGroup *group : this->channel_groups()) {
      group->fcurve_range_start = start_index;
      start_index += group->fcurve_range_length;
    }

    /* Double-check that this didn't push any of the groups off the end of the
     * fcurve array. */
    BLI_assert(start_index <= this->fcurve_array_num);
  }

  /* Recompute fcurves' group pointers. */
  {
    for (FCurve *fcurve : this->fcurves()) {
      fcurve->grp = nullptr;
    }
    for (bActionGroup *group : this->channel_groups()) {
      for (FCurve *fcurve : group->wrap().fcurves()) {
        fcurve->grp = group;
      }
    }
  }
}

bool ChannelGroup::is_legacy() const
{
  return this->channelbag == nullptr;
}

Span<FCurve *> ChannelGroup::fcurves()
{
  BLI_assert(!this->is_legacy());

  if (this->fcurve_range_length == 0) {
    return {};
  }

  return this->channelbag->wrap().fcurves().slice(this->fcurve_range_start,
                                                  this->fcurve_range_length);
}

Span<const FCurve *> ChannelGroup::fcurves() const
{
  BLI_assert(!this->is_legacy());

  if (this->fcurve_range_length == 0) {
    return {};
  }

  return this->channelbag->wrap().fcurves().slice(this->fcurve_range_start,
                                                  this->fcurve_range_length);
}

/* Utility function implementations. */

const animrig::Channelbag *channelbag_for_action_slot(const Action &action,
                                                      const slot_handle_t slot_handle)
{
  assert_baklava_phase_1_invariants(action);

  if (slot_handle == Slot::unassigned) {
    return nullptr;
  }

  for (const animrig::Layer *layer : action.layers()) {
    for (const animrig::Strip *strip : layer->strips()) {
      switch (strip->type()) {
        case animrig::Strip::Type::Keyframe: {
          const animrig::StripKeyframeData &strip_data = strip->data<animrig::StripKeyframeData>(
              action);
          const animrig::Channelbag *bag = strip_data.channelbag_for_slot(slot_handle);
          if (bag) {
            return bag;
          }
        }
      }
    }
  }

  return nullptr;
}

animrig::Channelbag *channelbag_for_action_slot(Action &action, const slot_handle_t slot_handle)
{
  const animrig::Channelbag *const_bag = channelbag_for_action_slot(
      const_cast<const Action &>(action), slot_handle);
  return const_cast<animrig::Channelbag *>(const_bag);
}

Span<FCurve *> fcurves_for_action_slot(Action &action, const slot_handle_t slot_handle)
{
  BLI_assert(action.is_action_layered());
  assert_baklava_phase_1_invariants(action);
  animrig::Channelbag *bag = channelbag_for_action_slot(action, slot_handle);
  if (!bag) {
    return {};
  }
  return bag->fcurves();
}

Span<const FCurve *> fcurves_for_action_slot(const Action &action, const slot_handle_t slot_handle)
{
  BLI_assert(action.is_action_layered());
  assert_baklava_phase_1_invariants(action);
  const animrig::Channelbag *bag = channelbag_for_action_slot(action, slot_handle);
  if (!bag) {
    return {};
  }
  return bag->fcurves();
}

FCurve *fcurve_find_in_action(bAction *act, const FCurveDescriptor &fcurve_descriptor)
{
  if (act == nullptr) {
    return nullptr;
  }

  Action &action = act->wrap();
  if (action.is_action_legacy()) {
    return BKE_fcurve_find(
        &act->curves, fcurve_descriptor.rna_path.c_str(), fcurve_descriptor.array_index);
  }

  assert_baklava_phase_1_invariants(action);
  Layer *layer = action.layer(0);
  if (!layer) {
    return nullptr;
  }
  Strip *strip = layer->strip(0);
  if (!strip) {
    return nullptr;
  }

  StripKeyframeData &strip_data = strip->data<StripKeyframeData>(action);

  for (Channelbag *channelbag : strip_data.channelbags()) {
    FCurve *fcu = channelbag->fcurve_find(fcurve_descriptor);
    if (fcu) {
      return fcu;
    }
  }

  return nullptr;
}

FCurve *fcurve_find_in_assigned_slot(AnimData &adt, const FCurveDescriptor &fcurve_descriptor)
{
  return fcurve_find_in_action_slot(adt.action, adt.slot_handle, fcurve_descriptor);
}

FCurve *fcurve_find_in_action_slot(bAction *act,
                                   const slot_handle_t slot_handle,
                                   const FCurveDescriptor &fcurve_descriptor)
{
  if (act == nullptr) {
    return nullptr;
  }

  Action &action = act->wrap();
  if (action.is_action_legacy()) {
    return BKE_fcurve_find(
        &act->curves, fcurve_descriptor.rna_path.c_str(), fcurve_descriptor.array_index);
  }

  Channelbag *cbag = channelbag_for_action_slot(action, slot_handle);
  if (!cbag) {
    return nullptr;
  }
  return cbag->fcurve_find(fcurve_descriptor);
}

bool fcurve_matches_collection_path(const FCurve &fcurve,
                                    const StringRefNull collection_rna_path,
                                    const StringRefNull data_name)
{
  BLI_assert(!collection_rna_path.is_empty());

  const size_t quoted_name_size = data_name.size() + 1;
  char *quoted_name = static_cast<char *>(alloca(quoted_name_size));

  if (!fcurve.rna_path) {
    return false;
  }
  /* Skipping names longer than `quoted_name_size` is OK since we're after an exact match. */
  if (!BLI_str_quoted_substr(
          fcurve.rna_path, collection_rna_path.c_str(), quoted_name, quoted_name_size))
  {
    return false;
  }
  if (quoted_name != data_name) {
    return false;
  }

  return true;
}

Vector<FCurve *> fcurves_in_action_slot_filtered(bAction *act,
                                                 const slot_handle_t slot_handle,
                                                 FunctionRef<bool(const FCurve &fcurve)> predicate)
{
  BLI_assert(act);

  Vector<FCurve *> found;

  foreach_fcurve_in_action_slot(act->wrap(), slot_handle, [&](FCurve &fcurve) {
    if (predicate(fcurve)) {
      found.append(&fcurve);
    }
  });

  return found;
}

Vector<FCurve *> fcurves_in_span_filtered(Span<FCurve *> fcurves,
                                          FunctionRef<bool(const FCurve &fcurve)> predicate)
{
  Vector<FCurve *> found;

  for (FCurve *fcurve : fcurves) {
    if (predicate(*fcurve)) {
      found.append(fcurve);
    }
  }

  return found;
}

Vector<FCurve *> fcurves_in_listbase_filtered(ListBase /* FCurve * */ fcurves,
                                              FunctionRef<bool(const FCurve &fcurve)> predicate)
{
  Vector<FCurve *> found;

  LISTBASE_FOREACH (FCurve *, fcurve, &fcurves) {
    if (predicate(*fcurve)) {
      found.append(fcurve);
    }
  }

  return found;
}

FCurve *action_fcurve_ensure_ex(Main *bmain,
                                bAction *act,
                                const char group[],
                                PointerRNA *ptr,
                                const FCurveDescriptor &fcurve_descriptor)
{
  if (act == nullptr) {
    return nullptr;
  }

  if (animrig::legacy::action_treat_as_legacy(*act)) {
    return action_fcurve_ensure_legacy(bmain, act, group, ptr, fcurve_descriptor);
  }

  /* NOTE: for layered actions we require the following:
   *
   * - `ptr` is non-null.
   * - `ptr` has an `owner_id` that already uses `act`.
   *
   * This isn't for any principled reason, but rather is because adding
   * support for layered actions to this function was a fix to make Follow
   * Path animation work properly with layered actions (see PR #124353), and
   * those are the requirements the Follow Path code conveniently met.
   * Moreover those requirements were also already met by the other call sites
   * that potentially call this function with layered actions.
   *
   * Trying to puzzle out what "should" happen when these requirements don't
   * hold, or if this is even the best place to handle the layered action
   * cases at all, was leading to discussion of larger changes than made sense
   * to tackle at that point. */
  BLI_assert(ptr != nullptr);
  if (ptr == nullptr || ptr->owner_id == nullptr) {
    return nullptr;
  }

  return &action_fcurve_ensure(bmain, *act, *ptr->owner_id, fcurve_descriptor);
}

Channelbag &action_channelbag_ensure(bAction &dna_action, ID &animated_id)
{
  Action &action = dna_action.wrap();
  BLI_assert(get_action(animated_id) == &action);

  /* Ensure the id has an assigned slot. */
  Slot *slot = assign_action_ensure_slot_for_keying(action, animated_id);
  /* A nullptr here means the ID type is not animatable. But since the Action is already assigned,
   * it is certain that the ID is actually animatable. */
  BLI_assert(slot);

  action.layer_keystrip_ensure();

  assert_baklava_phase_1_invariants(action);
  StripKeyframeData &strip_data = action.layer(0)->strip(0)->data<StripKeyframeData>(action);

  return strip_data.channelbag_for_slot_ensure(*slot);
}

FCurve &action_fcurve_ensure(Main *bmain,
                             bAction &dna_action,
                             ID &animated_id,
                             const FCurveDescriptor &fcurve_descriptor)
{
  Channelbag &channelbag = action_channelbag_ensure(dna_action, animated_id);
  return channelbag.fcurve_ensure(bmain, fcurve_descriptor);
}

FCurve *action_fcurve_ensure_legacy(Main *bmain,
                                    bAction *act,
                                    const char group[],
                                    PointerRNA *ptr,
                                    const FCurveDescriptor &fcurve_descriptor)
{
  if (!act) {
    return nullptr;
  }

  BLI_assert(act->wrap().is_empty() || act->wrap().is_action_legacy());

  /* Try to find f-curve matching for this setting.
   * - add if not found and allowed to add one
   *   TODO: add auto-grouping support? how this works will need to be resolved
   */
  FCurve *fcu = animrig::fcurve_find_in_action(act, fcurve_descriptor);

  if (fcu != nullptr) {
    return fcu;
  }

  /* Determine the property (sub)type if we can. */
  std::optional<PropertyType> prop_type = std::nullopt;
  std::optional<PropertySubType> prop_subtype = std::nullopt;
  if (ptr != nullptr) {
    PropertyRNA *resolved_prop;
    PointerRNA resolved_ptr;
    PointerRNA id_ptr = RNA_id_pointer_create(ptr->owner_id);
    const bool resolved = RNA_path_resolve_property(
        &id_ptr, fcurve_descriptor.rna_path.c_str(), &resolved_ptr, &resolved_prop);
    if (resolved) {
      prop_type = RNA_property_type(resolved_prop);
      prop_subtype = RNA_property_subtype(resolved_prop);
    }
  }

  BLI_assert_msg(!fcurve_descriptor.prop_type.has_value(),
                 "Did not expect a prop_type to be passed in. This is fine, but does need some "
                 "changes to action_fcurve_ensure_legacy() to deal with it");
  BLI_assert_msg(!fcurve_descriptor.prop_subtype.has_value(),
                 "Did not expect a prop_subtype to be passed in. This is fine, but does need some "
                 "changes to action_fcurve_ensure_legacy() to deal with it");
  fcu = create_fcurve_for_channel(
      {fcurve_descriptor.rna_path, fcurve_descriptor.array_index, prop_type, prop_subtype});

  if (BLI_listbase_is_empty(&act->curves)) {
    fcu->flag |= FCURVE_ACTIVE;
  }

  if (group) {
    bActionGroup *agrp = BKE_action_group_find_name(act, group);

    if (agrp == nullptr) {
      agrp = action_groups_add_new(act, group);

      /* Sync bone group colors if applicable. */
      if (ptr && (ptr->type == &RNA_PoseBone) && ptr->data) {
        const bPoseChannel *pchan = static_cast<const bPoseChannel *>(ptr->data);
        action_group_colors_set_from_posebone(agrp, pchan);
      }
    }

    action_groups_add_channel(act, agrp, fcu);
  }
  else {
    BLI_addtail(&act->curves, fcu);
  }

  /* New f-curve was added, meaning it's possible that it affects
   * dependency graph component which wasn't previously animated.
   */
  DEG_relations_tag_update(bmain);

  return fcu;
}

bool action_fcurve_remove(Action &action, FCurve &fcu)
{
  if (action_fcurve_detach(action, fcu)) {
    BKE_fcurve_free(&fcu);
    return true;
  }

  return false;
}

bool action_fcurve_detach(Action &action, FCurve &fcurve_to_detach)
{
  if (action.is_action_legacy()) {
    return BLI_remlink_safe(&action.curves, &fcurve_to_detach);
  }

  for (Layer *layer : action.layers()) {
    for (Strip *strip : layer->strips()) {
      if (!(strip->type() == Strip::Type::Keyframe)) {
        continue;
      }
      StripKeyframeData &strip_data = strip->data<StripKeyframeData>(action);
      for (Channelbag *bag : strip_data.channelbags()) {
        const bool is_detached = bag->fcurve_detach(fcurve_to_detach);
        if (is_detached) {
          return true;
        }
      }
    }
  }
  return false;
}

void action_fcurve_attach(Action &action,
                          const slot_handle_t action_slot,
                          FCurve &fcurve_to_attach,
                          std::optional<StringRefNull> group_name)
{
  if (animrig::legacy::action_treat_as_legacy(action)) {
    BLI_addtail(&action.curves, &fcurve_to_attach);
    return;
  }

  Slot *slot = action.slot_for_handle(action_slot);
  BLI_assert(slot);
  if (!slot) {
    printf("Cannot find slot handle %d on Action %s, unable to attach F-Curve %s[%d] to it!\n",
           action_slot,
           action.id.name + 2,
           fcurve_to_attach.rna_path,
           fcurve_to_attach.array_index);
    return;
  }

  action.layer_keystrip_ensure();
  StripKeyframeData &strip_data = action.layer(0)->strip(0)->data<StripKeyframeData>(action);
  Channelbag &cbag = strip_data.channelbag_for_slot_ensure(*slot);
  cbag.fcurve_append(fcurve_to_attach);

  if (group_name) {
    bActionGroup &group = cbag.channel_group_ensure(*group_name);
    cbag.fcurve_assign_to_channel_group(fcurve_to_attach, group);
  }
}

void action_fcurve_move(Action &action_dst,
                        const slot_handle_t action_slot_dst,
                        Action &action_src,
                        FCurve &fcurve)
{
  /* Store the group name locally, as the group will be removed if this was its
   * last F-Curve. */
  std::optional<std::string> group_name;
  if (fcurve.grp) {
    group_name = fcurve.grp->name;
  }

  const bool is_detached = action_fcurve_detach(action_src, fcurve);
  BLI_assert(is_detached);
  UNUSED_VARS_NDEBUG(is_detached);

  action_fcurve_attach(action_dst, action_slot_dst, fcurve, group_name);
}

void channelbag_fcurves_move(Channelbag &channelbag_dst, Channelbag &channelbag_src)
{
  while (!channelbag_src.fcurves().is_empty()) {
    FCurve &fcurve = *channelbag_src.fcurve(0);

    /* Store the group name locally, as the group will be removed if this was its
     * last F-Curve. */
    std::optional<std::string> group_name;
    if (fcurve.grp) {
      group_name = fcurve.grp->name;
    }

    const bool is_detached = channelbag_src.fcurve_detach(fcurve);
    BLI_assert(is_detached);
    UNUSED_VARS_NDEBUG(is_detached);

    channelbag_dst.fcurve_append(fcurve);

    if (group_name) {
      bActionGroup &group = channelbag_dst.channel_group_ensure(*group_name);
      channelbag_dst.fcurve_assign_to_channel_group(fcurve, group);
    }
  }
}

bool Channelbag::fcurve_assign_to_channel_group(FCurve &fcurve, bActionGroup &to_group)
{
  if (this->channel_groups().first_index_try(&to_group) == -1) {
    return false;
  }

  const int fcurve_index = this->fcurves().first_index_try(&fcurve);
  if (fcurve_index == -1) {
    return false;
  }

  if (fcurve.grp == &to_group) {
    return true;
  }

  /* Remove fcurve from old group, if it belongs to one. */
  if (fcurve.grp != nullptr) {
    fcurve.grp->fcurve_range_length--;
    if (fcurve.grp->fcurve_range_length == 0) {
      const int group_index = this->channel_groups().first_index_try(fcurve.grp);
      this->channel_group_remove_raw(group_index);
    }
    this->restore_channel_group_invariants();
  }

  array_shift_range(this->fcurve_array,
                    this->fcurve_array_num,
                    fcurve_index,
                    fcurve_index + 1,
                    to_group.fcurve_range_start + to_group.fcurve_range_length);
  to_group.fcurve_range_length++;

  this->restore_channel_group_invariants();

  return true;
}

bool Channelbag::fcurve_ungroup(FCurve &fcurve)
{
  const int fcurve_index = this->fcurves().first_index_try(&fcurve);
  if (fcurve_index == -1) {
    return false;
  }

  if (fcurve.grp == nullptr) {
    return true;
  }

  bActionGroup *old_group = fcurve.grp;

  array_shift_range(this->fcurve_array,
                    this->fcurve_array_num,
                    fcurve_index,
                    fcurve_index + 1,
                    this->fcurve_array_num - 1);

  old_group->fcurve_range_length--;
  if (old_group->fcurve_range_length == 0) {
    const int old_group_index = this->channel_groups().first_index_try(old_group);
    this->channel_group_remove_raw(old_group_index);
  }

  this->restore_channel_group_invariants();

  return true;
}

ID *action_slot_get_id_for_keying(Main &bmain,
                                  Action &action,
                                  const slot_handle_t slot_handle,
                                  ID *primary_id)
{
  if (animrig::legacy::action_treat_as_legacy(action)) {
    if (primary_id && get_action(*primary_id) == &action) {
      return primary_id;
    }
    return nullptr;
  }

  Slot *slot = action.slot_for_handle(slot_handle);
  if (slot == nullptr) {
    return nullptr;
  }

  blender::Span<ID *> users = slot->users(bmain);
  if (users.size() == 1) {
    /* We only do this for `users.size() == 1` and not `users.size() >= 1`
     * because when there's more than one user it's ambiguous which user we
     * should return, and that would be unpredictable for end users of Blender.
     * We also expect that to be a corner case anyway.  So instead we let that
     * case either get disambiguated by the primary ID in the case below, or
     * return null. */
    return users[0];
  }
  if (users.contains(primary_id)) {
    return primary_id;
  }

  return nullptr;
}

ID *action_slot_get_id_best_guess(Main &bmain, Slot &slot, ID *primary_id)
{
  blender::Span<ID *> users = slot.users(bmain);
  if (users.is_empty()) {
    return nullptr;
  }
  if (users.contains(primary_id)) {
    return primary_id;
  }
  return users[0];
}

slot_handle_t first_slot_handle(const ::bAction &dna_action)
{
  const Action &action = dna_action.wrap();
  if (action.slot_array_num == 0) {
    return Slot::unassigned;
  }
  return action.slot_array[0]->handle;
}

void assert_baklava_phase_1_invariants(const Action &action)
{
  if (action.is_action_legacy()) {
    return;
  }
  if (action.layers().is_empty()) {
    return;
  }
  BLI_assert(action.layers().size() == 1);

  assert_baklava_phase_1_invariants(*action.layer(0));
}

void assert_baklava_phase_1_invariants(const Layer &layer)
{
  if (layer.strips().is_empty()) {
    return;
  }
  BLI_assert(layer.strips().size() == 1);

  assert_baklava_phase_1_invariants(*layer.strip(0));
}

void assert_baklava_phase_1_invariants(const Strip &strip)
{
  UNUSED_VARS_NDEBUG(strip);
  BLI_assert(strip.type() == Strip::Type::Keyframe);
  BLI_assert(strip.is_infinite());
  BLI_assert(strip.frame_offset == 0.0);
}

Action *convert_to_layered_action(Main &bmain, const Action &legacy_action)
{
  if (!legacy_action.is_action_legacy()) {
    return nullptr;
  }

  std::string suffix = "_layered";
  /* In case the legacy action has a long name it is shortened to make space for the suffix. */
  char legacy_name[MAX_ID_NAME - 10];
  /* Offsetting the id.name to remove the ID prefix (AC) which gets added back later. */
  STRNCPY_UTF8(legacy_name, legacy_action.id.name + 2);

  const std::string layered_action_name = std::string(legacy_name) + suffix;
  bAction *dna_action = BKE_action_add(&bmain, layered_action_name.c_str());

  Action &converted_action = dna_action->wrap();
  Slot &slot = converted_action.slot_add();
  Layer &layer = converted_action.layer_add(legacy_action.id.name);
  Strip &strip = layer.strip_add(converted_action, Strip::Type::Keyframe);
  BLI_assert(strip.data<StripKeyframeData>(converted_action).channelbag_array_num == 0);
  Channelbag *bag = &strip.data<StripKeyframeData>(converted_action).channelbag_for_slot_add(slot);

  const int fcu_count = BLI_listbase_count(&legacy_action.curves);
  bag->fcurve_array = MEM_calloc_arrayN<FCurve *>(fcu_count, "Convert to layered action");
  bag->fcurve_array_num = fcu_count;

  int i = 0;
  blender::Map<FCurve *, FCurve *> old_new_fcurve_map;
  LISTBASE_FOREACH_INDEX (FCurve *, fcu, &legacy_action.curves, i) {
    bag->fcurve_array[i] = BKE_fcurve_copy(fcu);
    bag->fcurve_array[i]->grp = nullptr;
    old_new_fcurve_map.add(fcu, bag->fcurve_array[i]);
  }

  LISTBASE_FOREACH (bActionGroup *, group, &legacy_action.groups) {
    /* The resulting group might not have the same name, because the legacy system allowed
     * duplicate names while the new system ensures uniqueness. */
    bActionGroup &converted_group = bag->channel_group_create(group->name);
    LISTBASE_FOREACH (FCurve *, fcu, &group->channels) {
      if (fcu->grp != group) {
        /* Since the group listbase points to the action listbase, it won't stop iterating when
         * reaching the end of the group but iterate to the end of the action FCurves. */
        break;
      }
      FCurve *new_fcurve = old_new_fcurve_map.lookup(fcu);
      bag->fcurve_assign_to_channel_group(*new_fcurve, converted_group);
    }
  }

  return &converted_action;
}

/**
 * Clone information from the given slot into this slot while retaining important info like the
 * slot handle and runtime data. This copies the identifier which might clash with other
 * identifiers on the action. Call `slot_identifier_ensure_unique` after.
 */
static void clone_slot(const Slot &from, Slot &to)
{
  ActionSlotRuntimeHandle *runtime = to.runtime;
  slot_handle_t handle = to.handle;
  *reinterpret_cast<ActionSlot *>(&to) = *reinterpret_cast<const ActionSlot *>(&from);
  to.runtime = runtime;
  to.handle = handle;
}

void move_slot(Main &bmain, Slot &source_slot, Action &from_action, Action &to_action)
{
  BLI_assert(from_action.slots().contains(&source_slot));
  BLI_assert(&from_action != &to_action);

  /* No merging of strips or layers is handled. All data is put into the assumed single strip. */
  assert_baklava_phase_1_invariants(from_action);
  assert_baklava_phase_1_invariants(to_action);

  Slot &target_slot = to_action.slot_add();
  clone_slot(source_slot, target_slot);
  slot_identifier_ensure_unique(to_action, target_slot);

  if (!from_action.layers().is_empty() && !from_action.layer(0)->strips().is_empty()) {
    StripKeyframeData &from_strip_data = from_action.layer(0)->strip(0)->data<StripKeyframeData>(
        from_action);
    Channelbag *channelbag = from_strip_data.channelbag_for_slot(source_slot.handle);
    /* It's perfectly fine for a slot to not have a channelbag on each keyframe strip. */
    if (channelbag) {
      /* Only create the layer & keyframe strip if there is a channelbag to move
       * into it. Otherwise it's better to keep the Action lean, and defer their
       * creation when keys are inserted. */
      to_action.layer_keystrip_ensure();
      StripKeyframeData &to_strip_data = to_action.layer(0)->strip(0)->data<StripKeyframeData>(
          to_action);
      channelbag->slot_handle = target_slot.handle;
      grow_array_and_append<ActionChannelbag *>(
          &to_strip_data.channelbag_array, &to_strip_data.channelbag_array_num, channelbag);
      const int index = from_strip_data.find_channelbag_index(*channelbag);
      shrink_array_and_remove<ActionChannelbag *>(
          &from_strip_data.channelbag_array, &from_strip_data.channelbag_array_num, index);
    }
  }

  /* Reassign all users of `source_slot` to the action `to_action` and the slot `target_slot`. */
  for (ID *user : source_slot.users(bmain)) {
    const auto assign_other_action = [&](ID & /* animated_id */,
                                         bAction *&action_ptr_ref,
                                         slot_handle_t &slot_handle_ref,
                                         char *slot_identifier) -> bool {
      /* Only reassign if the reference is actually from the same action. Could be from a different
       * action when using the NLA or action constraints. */
      if (action_ptr_ref != &from_action) {
        return true;
      }

      { /* Assign the Action. */
        const bool assign_ok = generic_assign_action(
            *user, &to_action, action_ptr_ref, slot_handle_ref, slot_identifier);
        BLI_assert_msg(assign_ok, "Expecting slotted Actions to always be assignable");
        UNUSED_VARS_NDEBUG(assign_ok);
      }
      { /* Assign the Slot. */
        const ActionSlotAssignmentResult result = generic_assign_action_slot(
            &target_slot, *user, action_ptr_ref, slot_handle_ref, slot_identifier);
        BLI_assert(result == ActionSlotAssignmentResult::OK);
        UNUSED_VARS_NDEBUG(result);
      }

      /* TODO: move the tagging of animated IDs into generic_assign_action() and
       * generic_assign_action_slot(), as that's closer to the modification of
       * the animated ID.
       *
       * This line was added here for now, to fix #136388 with minimal impact on
       * other code, so that the fix can be easily back-ported to Blender 4.4. */
      DEG_id_tag_update(user, ID_RECALC_ANIMATION);
      return true;
    };
    foreach_action_slot_use_with_references(*user, assign_other_action);
  }

  from_action.slot_remove(source_slot);
}

Slot &duplicate_slot(Action &action, const Slot &slot)
{
  BLI_assert(action.slots().contains(const_cast<Slot *>(&slot)));

  /* Duplicate the slot itself. */
  Slot &cloned_slot = action.slot_add();
  clone_slot(slot, cloned_slot);
  slot_identifier_ensure_unique(action, cloned_slot);

  /* Duplicate each Channelbag for the source slot. */
  for (int i = 0; i < action.strip_keyframe_data_array_num; i++) {
    StripKeyframeData &strip_data = action.strip_keyframe_data_array[i]->wrap();
    strip_data.slot_data_duplicate(slot.handle, cloned_slot.handle);
  }

  /* The ID has changed, and so it needs to be re-evaluated. Animation does not
   * have to be flushed since nothing is using this slot yet. */
  DEG_id_tag_update(&action.id, ID_RECALC_ANIMATION_NO_FLUSH);

  return cloned_slot;
}

}  // namespace blender::animrig
