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

/** \file
 * \ingroup pythonintern
 *
 * This file defines the animation related methods used in `bpy_rna.cc`.
 */

#include <Python.h>
#include <cfloat> /* FLT_MAX */

#include "MEM_guardedalloc.h"

#include "BLI_string.h"
#include "BLI_string_utils.hh"
#include "BLI_utildefines.h"

#include "DNA_anim_types.h"
#include "DNA_scene_types.h"

#include "ED_keyframing.hh"

#include "ANIM_keyframing.hh"

#include "BKE_anim_data.hh"
#include "BKE_animsys.h"
#include "BKE_context.hh"
#include "BKE_fcurve.hh"
#include "BKE_global.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_report.hh"

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

#include "WM_api.hh"
#include "WM_types.hh"

#include "bpy_capi_utils.h"
#include "bpy_rna.h"
#include "bpy_rna_anim.h"

#include "../generic/py_capi_rna.h"
#include "../generic/python_utildefines.h"

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

/* for keyframes and drivers */
static int pyrna_struct_anim_args_parse_ex(PointerRNA *ptr,
                                           const char *error_prefix,
                                           const char *path,
                                           const char **r_path_full,
                                           int *r_index,
                                           bool *r_path_no_validate)
{
  const bool is_idbase = RNA_struct_is_ID(ptr->type);
  PropertyRNA *prop;
  PointerRNA r_ptr;

  if (ptr->data == nullptr) {
    PyErr_Format(
        PyExc_TypeError, "%.200s this struct has no data, can't be animated", error_prefix);
    return -1;
  }

  /* full paths can only be given from ID base */
  if (is_idbase) {
    int path_index = -1;
    if (RNA_path_resolve_property_full(ptr, path, &r_ptr, &prop, &path_index) == false) {
      prop = nullptr;
    }
    else if (path_index != -1) {
      PyErr_Format(PyExc_ValueError,
                   "%.200s path includes index, must be a separate argument",
                   error_prefix,
                   path);
      return -1;
    }
    else if (ptr->owner_id != r_ptr.owner_id) {
      PyErr_Format(PyExc_ValueError, "%.200s path spans ID blocks", error_prefix, path);
      return -1;
    }
  }
  else {
    prop = RNA_struct_find_property(ptr, path);
    r_ptr = *ptr;
  }

  if (prop == nullptr) {
    if (r_path_no_validate) {
      *r_path_no_validate = true;
      return -1;
    }
    PyErr_Format(PyExc_TypeError, "%.200s property \"%s\" not found", error_prefix, path);
    return -1;
  }

  if (r_path_no_validate) {
    /* Don't touch the index. */
  }
  else {
    if (!RNA_property_animateable(&r_ptr, prop)) {
      PyErr_Format(PyExc_TypeError, "%.200s property \"%s\" not animatable", error_prefix, path);
      return -1;
    }

    if (RNA_property_array_check(prop) == 0) {
      if ((*r_index) == -1) {
        *r_index = 0;
      }
      else {
        PyErr_Format(PyExc_TypeError,
                     "%.200s index %d was given while property \"%s\" is not an array",
                     error_prefix,
                     *r_index,
                     path);
        return -1;
      }
    }
    else {
      const int array_len = RNA_property_array_length(&r_ptr, prop);
      if ((*r_index) < -1 || (*r_index) >= array_len) {
        PyErr_Format(PyExc_TypeError,
                     "%.200s index out of range \"%s\", given %d, array length is %d",
                     error_prefix,
                     path,
                     *r_index,
                     array_len);
        return -1;
      }
    }
  }

  if (is_idbase) {
    *r_path_full = BLI_strdup(path);
  }
  else {
    const std::optional<std::string> path_full = RNA_path_from_ID_to_property(&r_ptr, prop);
    *r_path_full = path_full ? BLI_strdup(path_full->c_str()) : nullptr;

    if (*r_path_full == nullptr) {
      PyErr_Format(PyExc_TypeError, "%.200s could not make path to \"%s\"", error_prefix, path);
      return -1;
    }
  }

  return 0;
}

static int pyrna_struct_anim_args_parse(PointerRNA *ptr,
                                        const char *error_prefix,
                                        const char *path,
                                        const char **r_path_full,
                                        int *r_index)
{
  return pyrna_struct_anim_args_parse_ex(ptr, error_prefix, path, r_path_full, r_index, nullptr);
}

/**
 * Unlike #pyrna_struct_anim_args_parse \a r_path_full may be copied from \a path.
 */
static int pyrna_struct_anim_args_parse_no_resolve(PointerRNA *ptr,
                                                   const char *error_prefix,
                                                   const char *path,
                                                   const char **r_path_full)
{
  const bool is_idbase = RNA_struct_is_ID(ptr->type);
  if (is_idbase) {
    *r_path_full = path;
    return 0;
  }

  const std::optional<std::string> path_prefix = RNA_path_from_ID_to_struct(ptr);
  if (!path_prefix) {
    PyErr_Format(PyExc_TypeError,
                 "%.200s could not make path for type %s",
                 error_prefix,
                 RNA_struct_identifier(ptr->type));
    return -1;
  }

  if (*path == '[') {
    *r_path_full = BLI_string_joinN(path_prefix->c_str(), path);
  }
  else {
    *r_path_full = BLI_string_join_by_sep_charN('.', path_prefix->c_str(), path);
  }

  return 0;
}

static int pyrna_struct_anim_args_parse_no_resolve_fallback(PointerRNA *ptr,
                                                            const char *error_prefix,
                                                            const char *path,
                                                            const char **r_path_full,
                                                            int *r_index)
{
  bool path_unresolved = false;
  if (pyrna_struct_anim_args_parse_ex(
          ptr, error_prefix, path, r_path_full, r_index, &path_unresolved) == -1)
  {
    if (path_unresolved == true) {
      if (pyrna_struct_anim_args_parse_no_resolve(ptr, error_prefix, path, r_path_full) == -1) {
        return -1;
      }
    }
    else {
      return -1;
    }
  }
  return 0;
}

/* internal use for insert and delete */
static int pyrna_struct_keyframe_parse(PointerRNA *ptr,
                                       PyObject *args,
                                       PyObject *kw,
                                       const char *parse_str,
                                       const char *error_prefix,
                                       /* return values */
                                       const char **r_path_full,
                                       int *r_index,
                                       float *r_cfra,
                                       const char **r_group_name,
                                       int *r_options,
                                       eBezTriple_KeyframeType *r_keytype)
{
  static const char *kwlist[] = {
      "data_path", "index", "frame", "group", "options", "keytype", nullptr};
  PyObject *pyoptions = nullptr;
  char *keytype_name = nullptr;
  const char *path;

  /* NOTE: `parse_str` MUST start with `s|ifsO!`. */
  if (!PyArg_ParseTupleAndKeywords(args,
                                   kw,
                                   parse_str,
                                   (char **)kwlist,
                                   &path,
                                   r_index,
                                   r_cfra,
                                   r_group_name,
                                   &PySet_Type,
                                   &pyoptions,
                                   &keytype_name))
  {
    return -1;
  }

  if (pyrna_struct_anim_args_parse(ptr, error_prefix, path, r_path_full, r_index) == -1) {
    return -1;
  }

  if (*r_cfra == FLT_MAX) {
    *r_cfra = CTX_data_scene(BPY_context_get())->r.cfra;
  }

  /* flag may be null (no option currently for remove keyframes e.g.). */
  if (r_options) {
    if (pyoptions &&
        (pyrna_enum_bitfield_from_set(
             rna_enum_keying_flag_api_items, pyoptions, r_options, error_prefix) == -1))
    {
      return -1;
    }

    *r_options |= INSERTKEY_NO_USERPREF;
  }

  if (r_keytype) {
    int keytype_as_int = 0;
    if (keytype_name && pyrna_enum_value_from_id(rna_enum_beztriple_keyframe_type_items,
                                                 keytype_name,
                                                 &keytype_as_int,
                                                 error_prefix) == -1)
    {
      return -1;
    }
    *r_keytype = eBezTriple_KeyframeType(keytype_as_int);
  }

  return 0; /* success */
}

char pyrna_struct_keyframe_insert_doc[] =
    ".. method:: keyframe_insert(data_path, index=-1, frame=bpy.context.scene.frame_current, "
    "group=\"\", options=set(), keytype='KEYFRAME')\n"
    "\n"
    "   Insert a keyframe on the property given, adding fcurves and animation data when "
    "necessary.\n"
    "\n"
    "   :arg data_path: path to the property to key, analogous to the fcurve's data path.\n"
    "   :type data_path: string\n"
    "   :arg index: array index of the property to key.\n"
    "      Defaults to -1 which will key all indices or a single channel if the property is not "
    "an array.\n"
    "   :type index: int\n"
    "   :arg frame: The frame on which the keyframe is inserted, defaulting to the current "
    "frame.\n"
    "   :type frame: float\n"
    "   :arg group: The name of the group the F-Curve should be added to if it doesn't exist "
    "yet.\n"
    "   :type group: str\n"
    "   :arg options: Optional set of flags:\n"
    "\n"
    "      - ``INSERTKEY_NEEDED`` Only insert keyframes where they're needed in the relevant "
    "F-Curves.\n"
    "      - ``INSERTKEY_VISUAL`` Insert keyframes based on 'visual transforms'.\n"
    "      - ``INSERTKEY_XYZ_TO_RGB`` This flag is no longer in use, and is here so that code "
    "that uses it doesn't break. The XYZ=RGB coloring is determined by the animation "
    "preferences.\n"
    "      - ``INSERTKEY_REPLACE`` Only replace already existing keyframes.\n"
    "      - ``INSERTKEY_AVAILABLE`` Only insert into already existing F-Curves.\n"
    "      - ``INSERTKEY_CYCLE_AWARE`` Take cyclic extrapolation into account "
    "(Cycle-Aware Keying option).\n"
    "   :type options: set\n"
    "   :arg keytype: Type of the key: 'KEYFRAME', 'BREAKDOWN', 'MOVING_HOLD', 'EXTREME', "
    "'JITTER', or 'GENERATED'\n"
    "   :type keytype: string\n"
    "   :return: Success of keyframe insertion.\n"
    "   :rtype: boolean\n";
PyObject *pyrna_struct_keyframe_insert(BPy_StructRNA *self, PyObject *args, PyObject *kw)
{
  using namespace blender::animrig;
  /* args, pyrna_struct_keyframe_parse handles these */
  const char *path_full = nullptr;
  int index = -1;
  float cfra = FLT_MAX;
  const char *group_name = nullptr;
  eBezTriple_KeyframeType keytype = BEZT_KEYTYPE_KEYFRAME;
  int options = 0;

  PYRNA_STRUCT_CHECK_OBJ(self);

  if (pyrna_struct_keyframe_parse(&self->ptr,
                                  args,
                                  kw,
                                  "s|$ifsO!s:bpy_struct.keyframe_insert()",
                                  "bpy_struct.keyframe_insert()",
                                  &path_full,
                                  &index,
                                  &cfra,
                                  &group_name,
                                  &options,
                                  &keytype) == -1)
  {
    return nullptr;
  }

  ReportList reports;
  bool result = false;

  BKE_reports_init(&reports, RPT_STORE);

  /* This assumes that keyframes are only added on original data & using the active depsgraph. If
   * it turns out to be necessary for some reason to insert keyframes on evaluated objects, we can
   * revisit this and add an explicit `depsgraph` keyword argument to the function call.
   *
   * The depsgraph is only used for evaluating the NLA so this might not be needed in the future.
   */
  bContext *C = BPY_context_get();
  Depsgraph *depsgraph = CTX_data_depsgraph_pointer(C);
  const AnimationEvalContext anim_eval_context = BKE_animsys_eval_context_construct(depsgraph,
                                                                                    cfra);

  if (self->ptr.type == &RNA_NlaStrip) {
    /* Handle special properties for NLA Strips, whose F-Curves are stored on the
     * strips themselves. These are stored separately or else the properties will
     * not have any effect.
     */

    PointerRNA ptr = self->ptr;
    PropertyRNA *prop = nullptr;
    const char *prop_name;

    /* Retrieve the property identifier from the full path, since we can't get it any other way */
    prop_name = strrchr(path_full, '.');
    if ((prop_name >= path_full) && (prop_name + 1 < path_full + strlen(path_full))) {
      prop = RNA_struct_find_property(&ptr, prop_name + 1);
    }

    if (prop) {
      NlaStrip *strip = static_cast<NlaStrip *>(ptr.data);
      FCurve *fcu = BKE_fcurve_find(&strip->fcurves, RNA_property_identifier(prop), index);
      result = insert_keyframe_direct(&reports,
                                      ptr,
                                      prop,
                                      fcu,
                                      &anim_eval_context,
                                      eBezTriple_KeyframeType(keytype),
                                      nullptr,
                                      eInsertKeyFlags(options));
    }
    else {
      BKE_reportf(&reports, RPT_ERROR, "Could not resolve path (%s)", path_full);
    }
  }
  else {
    ID *id = self->ptr.owner_id;

    BLI_assert(BKE_id_is_in_global_main(id));
    CombinedKeyingResult combined_result = insert_keyframe(G_MAIN,
                                                           *id,
                                                           group_name,
                                                           path_full,
                                                           index,
                                                           &anim_eval_context,
                                                           eBezTriple_KeyframeType(keytype),
                                                           eInsertKeyFlags(options));
    const int success_count = combined_result.get_count(SingleKeyingResult::SUCCESS);
    if (success_count == 0) {
      /* Ideally this would use the GUI presentation of RPT_ERROR, as the resulting pop-up has more
       * vertical space than the single-line warning in the status bar. However, semantically these
       * may not be errors at all, as skipping the keying of certain properties due to the 'only
       * insert available' flag is not an error.
       *
       * Furthermore, using RPT_ERROR here would cause this function to raise a Python exception,
       * rather than returning a boolean. */
      combined_result.generate_reports(&reports, RPT_WARNING);
    }
    result = success_count != 0;
  }

  MEM_freeN((void *)path_full);

  if (BPy_reports_to_error(&reports, PyExc_RuntimeError, false) == -1) {
    BKE_reports_free(&reports);
    return nullptr;
  }
  BPy_reports_write_stdout(&reports, nullptr);
  BKE_reports_free(&reports);

  if (result) {
    WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, nullptr);
  }

  return PyBool_FromLong(result);
}

char pyrna_struct_keyframe_delete_doc[] =
    ".. method:: keyframe_delete(data_path, index=-1, frame=bpy.context.scene.frame_current, "
    "group=\"\")\n"
    "\n"
    "   Remove a keyframe from this properties fcurve.\n"
    "\n"
    "   :arg data_path: path to the property to remove a key, analogous to the fcurve's data "
    "path.\n"
    "   :type data_path: string\n"
    "   :arg index: array index of the property to remove a key. Defaults to -1 removing all "
    "indices or a single channel if the property is not an array.\n"
    "   :type index: int\n"
    "   :arg frame: The frame on which the keyframe is deleted, defaulting to the current frame.\n"
    "   :type frame: float\n"
    "   :arg group: The name of the group the F-Curve should be added to if it doesn't exist "
    "yet.\n"
    "   :type group: str\n"
    "   :return: Success of keyframe deletion.\n"
    "   :rtype: boolean\n";
PyObject *pyrna_struct_keyframe_delete(BPy_StructRNA *self, PyObject *args, PyObject *kw)
{
  /* args, pyrna_struct_keyframe_parse handles these */
  const char *path_full = nullptr;
  int index = -1;
  float cfra = FLT_MAX;
  const char *group_name = nullptr;

  PYRNA_STRUCT_CHECK_OBJ(self);

  if (pyrna_struct_keyframe_parse(&self->ptr,
                                  args,
                                  kw,
                                  "s|$ifsOs!:bpy_struct.keyframe_delete()",
                                  "bpy_struct.keyframe_insert()",
                                  &path_full,
                                  &index,
                                  &cfra,
                                  &group_name,
                                  nullptr,
                                  nullptr) == -1)
  {
    return nullptr;
  }

  ReportList reports;
  bool result = false;

  BKE_reports_init(&reports, RPT_STORE);

  if (self->ptr.type == &RNA_NlaStrip) {
    /* Handle special properties for NLA Strips, whose F-Curves are stored on the
     * strips themselves. These are stored separately or else the properties will
     * not have any effect.
     */

    PointerRNA ptr = self->ptr;
    PropertyRNA *prop = nullptr;
    const char *prop_name;

    /* Retrieve the property identifier from the full path, since we can't get it any other way */
    prop_name = strrchr(path_full, '.');
    if ((prop_name >= path_full) && (prop_name + 1 < path_full + strlen(path_full))) {
      prop = RNA_struct_find_property(&ptr, prop_name + 1);
    }

    if (prop) {
      ID *id = ptr.owner_id;
      NlaStrip *strip = static_cast<NlaStrip *>(ptr.data);
      FCurve *fcu = BKE_fcurve_find(&strip->fcurves, RNA_property_identifier(prop), index);

      /* NOTE: This should be true, or else we wouldn't be able to get here. */
      BLI_assert(fcu != nullptr);

      if (BKE_fcurve_is_protected(fcu)) {
        BKE_reportf(
            &reports,
            RPT_WARNING,
            "Not deleting keyframe for locked F-Curve for NLA Strip influence on %s - %s '%s'",
            strip->name,
            BKE_idtype_idcode_to_name(GS(id->name)),
            id->name + 2);
      }
      else {
        /* remove the keyframe directly
         * NOTE: cannot use delete_keyframe_fcurve(), as that will free the curve,
         *       and delete_keyframe() expects the FCurve to be part of an action
         */
        bool found = false;
        int i;

        /* try to find index of beztriple to get rid of */
        i = BKE_fcurve_bezt_binarysearch_index(fcu->bezt, cfra, fcu->totvert, &found);
        if (found) {
          /* delete the key at the index (will sanity check + do recalc afterwards) */
          BKE_fcurve_delete_key(fcu, i);
          BKE_fcurve_handles_recalc(fcu);
          result = true;
        }
      }
    }
    else {
      BKE_reportf(&reports, RPT_ERROR, "Could not resolve path (%s)", path_full);
    }
  }
  else {
    result = (blender::animrig::delete_keyframe(
                  G.main, &reports, self->ptr.owner_id, nullptr, path_full, index, cfra) != 0);
  }

  MEM_freeN((void *)path_full);

  if (BPy_reports_to_error(&reports, PyExc_RuntimeError, true) == -1) {
    return nullptr;
  }

  return PyBool_FromLong(result);
}

char pyrna_struct_driver_add_doc[] =
    ".. method:: driver_add(path, index=-1)\n"
    "\n"
    "   Adds driver(s) to the given property\n"
    "\n"
    "   :arg path: path to the property to drive, analogous to the fcurve's data path.\n"
    "   :type path: string\n"
    "   :arg index: array index of the property drive. Defaults to -1 for all indices or a single "
    "channel if the property is not an array.\n"
    "   :type index: int\n"
    "   :return: The driver(s) added.\n"
    "   :rtype: :class:`bpy.types.FCurve` or list if index is -1 with an array property.\n";
PyObject *pyrna_struct_driver_add(BPy_StructRNA *self, PyObject *args)
{
  const char *path, *path_full;
  int index = -1;

  PYRNA_STRUCT_CHECK_OBJ(self);

  if (!PyArg_ParseTuple(args, "s|i:driver_add", &path, &index)) {
    return nullptr;
  }

  if (pyrna_struct_anim_args_parse(
          &self->ptr, "bpy_struct.driver_add():", path, &path_full, &index) == -1)
  {
    return nullptr;
  }

  PyObject *ret = nullptr;
  ReportList reports;
  int result;

  BKE_reports_init(&reports, RPT_STORE);

  result = ANIM_add_driver(&reports,
                           (ID *)self->ptr.owner_id,
                           path_full,
                           index,
                           CREATEDRIVER_WITH_FMODIFIER,
                           DRIVER_TYPE_PYTHON);

  if (BPy_reports_to_error(&reports, PyExc_RuntimeError, true) == -1) {
    return nullptr;
  }

  if (result) {
    ID *id = self->ptr.owner_id;
    AnimData *adt = BKE_animdata_from_id(id);
    FCurve *fcu;

    PointerRNA tptr;

    if (index == -1) { /* all, use a list */
      int i = 0;
      ret = PyList_New(0);
      while ((fcu = BKE_fcurve_find(&adt->drivers, path_full, i++))) {
        tptr = RNA_pointer_create(id, &RNA_FCurve, fcu);
        PyList_APPEND(ret, pyrna_struct_CreatePyObject(&tptr));
      }
    }
    else {
      fcu = BKE_fcurve_find(&adt->drivers, path_full, index);
      tptr = RNA_pointer_create(id, &RNA_FCurve, fcu);
      ret = pyrna_struct_CreatePyObject(&tptr);
    }

    bContext *context = BPY_context_get();
    WM_event_add_notifier(BPY_context_get(), NC_ANIMATION | ND_FCURVES_ORDER, nullptr);
    DEG_id_tag_update(id, ID_RECALC_SYNC_TO_EVAL);
    DEG_relations_tag_update(CTX_data_main(context));
  }
  else {
    /* XXX: should be handled by reports. */
    PyErr_SetString(PyExc_TypeError,
                    "bpy_struct.driver_add(): failed because of an internal error");
    return nullptr;
  }

  MEM_freeN((void *)path_full);

  return ret;
}

char pyrna_struct_driver_remove_doc[] =
    ".. method:: driver_remove(path, index=-1)\n"
    "\n"
    "   Remove driver(s) from the given property\n"
    "\n"
    "   :arg path: path to the property to drive, analogous to the fcurve's data path.\n"
    "   :type path: string\n"
    "   :arg index: array index of the property drive. Defaults to -1 for all indices or a single "
    "channel if the property is not an array.\n"
    "   :type index: int\n"
    "   :return: Success of driver removal.\n"
    "   :rtype: boolean\n";
PyObject *pyrna_struct_driver_remove(BPy_StructRNA *self, PyObject *args)
{
  const char *path, *path_full;
  int index = -1;

  PYRNA_STRUCT_CHECK_OBJ(self);

  if (!PyArg_ParseTuple(args, "s|i:driver_remove", &path, &index)) {
    return nullptr;
  }

  if (pyrna_struct_anim_args_parse_no_resolve_fallback(
          &self->ptr, "bpy_struct.driver_remove():", path, &path_full, &index) == -1)
  {
    return nullptr;
  }

  short result;
  ReportList reports;

  BKE_reports_init(&reports, RPT_STORE);

  result = ANIM_remove_driver(self->ptr.owner_id, path_full, index);

  if (path != path_full) {
    MEM_freeN((void *)path_full);
  }

  if (BPy_reports_to_error(&reports, PyExc_RuntimeError, true) == -1) {
    return nullptr;
  }

  bContext *context = BPY_context_get();
  WM_event_add_notifier(context, NC_ANIMATION | ND_FCURVES_ORDER, nullptr);
  DEG_relations_tag_update(CTX_data_main(context));

  return PyBool_FromLong(result);
}
