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

/** \file
 * \ingroup spgraph
 *
 * Graph editor space & buttons.
 */

#include <cfloat>
#include <cstring>

#include "DNA_anim_enums.h"
#include "DNA_anim_types.h"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"

#include "MEM_guardedalloc.h"

#include "BLI_listbase.h"
#include "BLI_math_rotation.h"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include "BLI_utildefines.h"

#include "BLT_translation.hh"

#include "BKE_anim_data.hh"
#include "BKE_context.hh"
#include "BKE_curve.hh"
#include "BKE_fcurve.hh"
#include "BKE_fcurve_driver.h"
#include "BKE_global.hh"
#include "BKE_screen.hh"
#include "BKE_unit.hh"

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

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

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

#include "ED_anim_api.hh"
#include "ED_screen.hh"
#include "ED_undo.hh"

#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"

#include "graph_intern.hh" /* own include */

namespace blender {

#define B_REDR 1

/* -------------------------------------------------------------------- */
/** \name Internal Utilities
 * \{ */

static bool graph_panel_context(const bContext *C, bAnimListElem **ale, FCurve **fcu)
{
  bAnimContext ac;
  bAnimListElem *elem = nullptr;

  /* For now, only draw if we could init the anim-context info
   * (necessary for all animation-related tools)
   * to work correctly is able to be correctly retrieved.
   * There's no point showing empty panels?
   */
  if (ANIM_animdata_get_context(C, &ac) == 0) {
    return false;
  }

  /* try to find 'active' F-Curve */
  elem = get_active_fcurve_channel(&ac);
  if (elem == nullptr) {
    return false;
  }

  if (fcu) {
    *fcu = static_cast<FCurve *>(elem->data);
  }
  if (ale) {
    *ale = elem;
  }
  else {
    MEM_delete(elem);
  }

  return true;
}

FCurve *ANIM_graph_context_fcurve(const bContext *C)
{
  FCurve *fcu;
  if (!graph_panel_context(C, nullptr, &fcu)) {
    return nullptr;
  }

  return fcu;
}

static bool graph_panel_poll(const bContext *C, PanelType * /*pt*/)
{
  return graph_panel_context(C, nullptr, nullptr);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Cursor Header
 * \{ */

static void graph_panel_cursor_header(const bContext *C, Panel *panel)
{
  bScreen *screen = CTX_wm_screen(C);
  SpaceGraph *sipo = CTX_wm_space_graph(C);

  /* get RNA pointers for use when creating the UI elements */
  PointerRNA spaceptr = RNA_pointer_create_discrete(&screen->id, RNA_SpaceGraphEditor, sipo);

  /* 2D-Cursor */
  ui::Layout &col = panel->layout->column(false);
  col.prop(&spaceptr, "show_cursor", UI_ITEM_NONE, "", ICON_NONE);
}

static void graph_panel_cursor(const bContext *C, Panel *panel)
{
  bScreen *screen = CTX_wm_screen(C);
  SpaceGraph *sipo = CTX_wm_space_graph(C);
  Scene *scene = CTX_data_scene(C);
  ui::Layout &layout = *panel->layout;

  /* get RNA pointers for use when creating the UI elements */
  PointerRNA sceneptr = RNA_id_pointer_create(&scene->id);
  PointerRNA spaceptr = RNA_pointer_create_discrete(&screen->id, RNA_SpaceGraphEditor, sipo);

  layout.use_property_split_set(true);
  layout.use_property_decorate_set(false);

  /* 2D-Cursor */
  ui::Layout &col = layout.column(false);
  col.active_set(RNA_boolean_get(&spaceptr, "show_cursor"));

  ui::Layout &pos_sub = col.column(true);
  if (sipo->mode == SIPO_MODE_DRIVERS) {
    pos_sub.prop(&spaceptr, "cursor_position_x", UI_ITEM_NONE, IFACE_("Cursor X"), ICON_NONE);
  }
  else {
    pos_sub.prop(&sceneptr, "frame_current", UI_ITEM_NONE, IFACE_("Cursor X"), ICON_NONE);
  }

  pos_sub.prop(&spaceptr, "cursor_position_y", UI_ITEM_NONE, IFACE_("Y"), ICON_NONE);

  ui::Layout &sub = col.column(true);
  sub.op("GRAPH_OT_frame_jump", IFACE_("Cursor to Selection"), ICON_NONE);
  sub.op("GRAPH_OT_snap_cursor_value", IFACE_("Cursor Value to Selection"), ICON_NONE);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Active F-Curve
 * \{ */

static void graph_panel_properties(const bContext *C, Panel *panel)
{
  bAnimListElem *ale;
  FCurve *fcu;
  ui::Layout &layout = *panel->layout;
  char name[256];
  int icon = 0;

  if (!graph_panel_context(C, &ale, &fcu)) {
    return;
  }

  /* F-Curve pointer */
  PointerRNA fcu_ptr = RNA_pointer_create_discrete(ale->fcurve_owner_id, RNA_FCurve, fcu);

  /* user-friendly 'name' for F-Curve */
  ui::Layout &name_col = layout.column(false);
  if (ale->type == ANIMTYPE_FCURVE) {
    /* get user-friendly name for F-Curve */
    const std::optional<int> optional_icon = getname_anim_fcurve(name, ale->id, fcu);
    if (optional_icon) {
      icon = *optional_icon;
    }
    else if (ale->id) {
      icon = RNA_struct_ui_icon(ID_code_to_RNA_type(GS(ale->id->name)));
    }
    else {
      icon = ICON_NONE;
    }
  }
  else {
    /* NLA Control Curve, etc. */
    const bAnimChannelType *acf = ANIM_channel_get_typeinfo(ale);

    /* get name */
    if (acf && acf->name) {
      acf->name(ale, name);
    }
    else {
      STRNCPY_UTF8(name, IFACE_("<invalid>"));
      icon = ICON_ERROR;
    }

    /* icon */
    if (ale->type == ANIMTYPE_NLACURVE) {
      icon = ICON_NLA;
    }
  }
  name_col.label(name, icon);

  layout.use_property_split_set(true);
  layout.use_property_decorate_set(false);

  /* RNA-Path Editing - only really should be enabled when things aren't working */
  ui::Layout &rna_col = layout.column(false);
  rna_col.enabled_set((fcu->flag & FCURVE_DISABLED) != 0);
  rna_col.prop(&fcu_ptr, "data_path", UI_ITEM_NONE, "", ICON_RNA);
  rna_col.prop(&fcu_ptr, "array_index", UI_ITEM_NONE, std::nullopt, ICON_NONE);

  /* color settings */
  ui::Layout &color_col = layout.column(true);
  color_col.prop(&fcu_ptr, "color_mode", UI_ITEM_NONE, IFACE_("Display Color"), ICON_NONE);

  if (fcu->color_mode == FCURVE_COLOR_CUSTOM) {
    color_col.prop(&fcu_ptr, "color", UI_ITEM_NONE, IFACE_("Color"), ICON_NONE);
  }

  /* smoothing setting */
  ui::Layout &smooth_col = layout.column(true);
  smooth_col.prop(&fcu_ptr, "auto_smoothing", UI_ITEM_NONE, IFACE_("Handle Smoothing"), ICON_NONE);

  MEM_delete(ale);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Active Keyframe
 * \{ */

/* get 'active' keyframe for panel editing */
static bool get_active_fcurve_keyframe_edit(const FCurve *fcu,
                                            BezTriple **r_bezt,
                                            BezTriple **r_prevbezt)
{
  /* zero the pointers */
  *r_bezt = *r_prevbezt = nullptr;

  const int active_keyframe_index = BKE_fcurve_active_keyframe_index(fcu);
  if (active_keyframe_index == FCURVE_ACTIVE_KEYFRAME_NONE) {
    return false;
  }

  /* The active keyframe should be selected. */
  BLI_assert(BEZT_ISSEL_ANY(&fcu->bezt[active_keyframe_index]));

  *r_bezt = &fcu->bezt[active_keyframe_index];
  /* Previous is either one before the active, or the point itself if it's the first. */
  const int prev_index = max_ii(active_keyframe_index - 1, 0);
  *r_prevbezt = &fcu->bezt[prev_index];

  return true;
}

/* update callback for active keyframe properties - base updates stuff */
static void graphedit_activekey_update_cb(bContext * /*C*/, void *fcu_ptr, void * /*bezt_ptr*/)
{
  FCurve *fcu = static_cast<FCurve *>(fcu_ptr);

  /* make sure F-Curve and its handles are still valid after this editing */
  sort_time_fcurve(*fcu);
  BKE_fcurve_handles_recalc(*fcu);
}

/* update callback for active keyframe properties - handle-editing wrapper */
static void graphedit_activekey_handles_cb(bContext *C, void *fcu_ptr, void *bezt_ptr)
{
  BezTriple *bezt = static_cast<BezTriple *>(bezt_ptr);

  /* since editing the handles, make sure they're set to types which are receptive to editing
   * see transform_conversions.c :: createTransGraphEditData(), last step in second loop
   */
  if (ELEM(bezt->h1, HD_AUTO, HD_AUTO_ANIM) && ELEM(bezt->h2, HD_AUTO, HD_AUTO_ANIM)) {
    /* by changing to aligned handles, these can now be moved... */
    bezt->h1 = HD_ALIGN;
    bezt->h2 = HD_ALIGN;
  }
  else {
    BKE_nurb_bezt_handle_test(bezt, SELECT, NURB_HANDLE_TEST_EACH, false);
  }

  /* now call standard updates */
  graphedit_activekey_update_cb(C, fcu_ptr, bezt_ptr);
}

static void graphedit_activekey_handle_left_cb(bContext *C, void *fcu_ptr, void *bezt_ptr)
{
  BKE_fcurve_update_handle_flag_from_opposite(*static_cast<BezTriple *>(bezt_ptr),
                                              HandleSide::LEFT);
  graphedit_activekey_update_cb(C, fcu_ptr, bezt_ptr);
}

static void graphedit_activekey_handle_right_cb(bContext *C, void *fcu_ptr, void *bezt_ptr)
{
  BKE_fcurve_update_handle_flag_from_opposite(*static_cast<BezTriple *>(bezt_ptr),
                                              HandleSide::RIGHT);
  graphedit_activekey_update_cb(C, fcu_ptr, bezt_ptr);
}

/* update callback for editing coordinates of right handle in active keyframe properties
 * NOTE: we cannot just do graphedit_activekey_handles_cb() due to "order of computation"
 *       weirdness (see calchandleNurb_intern() and #39911)
 */
static void graphedit_activekey_left_handle_coord_cb(bContext *C, void *fcu_ptr, void *bezt_ptr)
{
  BezTriple *bezt = static_cast<BezTriple *>(bezt_ptr);

  const eBezTriple_Flag f1 = bezt->f1;
  const eBezTriple_Flag f3 = bezt->f3;

  bezt->f1 |= BEZT_FLAG_SELECT;
  bezt->f3 &= ~BEZT_FLAG_SELECT;

  /* perform normal updates NOW */
  graphedit_activekey_handles_cb(C, fcu_ptr, bezt_ptr);

  /* restore selection state so that no one notices this hack */
  bezt->f1 = f1;
  bezt->f3 = f3;
}

static void graphedit_activekey_right_handle_coord_cb(bContext *C, void *fcu_ptr, void *bezt_ptr)
{
  BezTriple *bezt = static_cast<BezTriple *>(bezt_ptr);

  /* original state of handle selection - to be restored after performing the recalculation */
  const eBezTriple_Flag f1 = bezt->f1;
  const eBezTriple_Flag f3 = bezt->f3;

  /* temporarily make it so that only the right handle is selected, so that updates go correctly
   * (i.e. it now acts as if we've just transforming the vert when it is selected by itself)
   */
  bezt->f1 &= ~BEZT_FLAG_SELECT;
  bezt->f3 |= BEZT_FLAG_SELECT;

  /* perform normal updates NOW */
  graphedit_activekey_handles_cb(C, fcu_ptr, bezt_ptr);

  /* restore selection state so that no one notices this hack */
  bezt->f1 = f1;
  bezt->f3 = f3;
}

static void graph_panel_key_properties(const bContext *C, Panel *panel)
{
  bAnimListElem *ale;
  FCurve *fcu;
  BezTriple *bezt, *prevbezt;

  ui::Layout &layout = *panel->layout;
  const ARegion *region = CTX_wm_region(C);
  /* Just a width big enough so buttons use entire layout width (will be clamped by it then). */
  const int but_max_width = region->winx;

  if (!graph_panel_context(C, &ale, &fcu)) {
    return;
  }

  ui::Block *block = layout.block();
  // block_func_handle_set(block, do_graph_region_buttons, nullptr);
  layout.use_property_split_set(true);
  layout.use_property_decorate_set(false);

  /* only show this info if there are keyframes to edit */
  if (get_active_fcurve_keyframe_edit(fcu, &bezt, &prevbezt)) {
    PointerRNA fcu_prop_ptr;
    PropertyRNA *fcu_prop = nullptr;
    ui::Button *but;
    int unit = B_UNIT_NONE;

    /* RNA pointer to keyframe, to allow editing */
    PointerRNA bezt_ptr = RNA_pointer_create_discrete(ale->fcurve_owner_id, RNA_Keyframe, bezt);

    /* get property that F-Curve affects, for some unit-conversion magic */
    PointerRNA id_ptr = RNA_id_pointer_create(ale->id);
    if (RNA_path_resolve_property(&id_ptr, fcu->rna_path, &fcu_prop_ptr, &fcu_prop)) {
      /* determine the unit for this property */
      unit = RNA_SUBTYPE_UNIT(RNA_property_subtype(fcu_prop));
    }

    /* interpolation */
    ui::Layout &interpolation_col = layout.column(false);
    if (fcu->flag & FCURVE_DISCRETE_VALUES) {
      ui::Layout &split = interpolation_col.split(0.33f, true);
      split.label(IFACE_("Interpolation:"), ICON_NONE);
      split.label(IFACE_("None for Enum/Boolean"), ICON_IPO_CONSTANT);
    }
    else {
      interpolation_col.prop(&bezt_ptr, "interpolation", UI_ITEM_NONE, std::nullopt, ICON_NONE);
    }

    /* easing type */
    if (bezt->ipo > BEZT_IPO_BEZ) {
      interpolation_col.prop(&bezt_ptr, "easing", UI_ITEM_NONE, std::nullopt, ICON_NONE);
    }

    /* easing extra */
    switch (bezt->ipo) {
      case BEZT_IPO_BACK: {
        ui::Layout &col = layout.column(true);
        col.prop(&bezt_ptr, "back", UI_ITEM_NONE, std::nullopt, ICON_NONE);
        break;
      }
      case BEZT_IPO_ELASTIC: {
        ui::Layout &col = layout.column(true);
        col.prop(&bezt_ptr, "amplitude", UI_ITEM_NONE, std::nullopt, ICON_NONE);
        col.prop(&bezt_ptr, "period", UI_ITEM_NONE, std::nullopt, ICON_NONE);
        break;
      }
      default:
        break;
    }

    /* numerical coordinate editing
     * - we use the button-versions of the calls so that we can attach special update handlers
     *   and unit conversion magic that cannot be achieved using a purely RNA-approach
     */
    /* keyframe itself */
    {
      ui::Layout &col = layout.column(true);
      uiItemL_respect_property_split(&col, IFACE_("Key Frame"), ICON_NONE);
      but = uiDefButR(block,
                      ui::ButtonType::Num,
                      "",
                      0,
                      0,
                      but_max_width,
                      UI_UNIT_Y,
                      &bezt_ptr,
                      "co_ui",
                      0,
                      0,
                      0,
                      std::nullopt);
      button_retval_set(but, B_REDR);
      button_func_set(but, graphedit_activekey_update_cb, fcu, bezt);

      uiItemL_respect_property_split(&col, IFACE_("Value"), ICON_NONE);
      but = uiDefButR(block,
                      ui::ButtonType::Num,
                      "",
                      0,
                      0,
                      but_max_width,
                      UI_UNIT_Y,
                      &bezt_ptr,
                      "co_ui",
                      1,
                      0,
                      0,
                      std::nullopt);
      button_retval_set(but, B_REDR);
      button_func_set(but, graphedit_activekey_update_cb, fcu, bezt);
      button_unit_type_set(but, unit);
    }

    /* previous handle - only if previous was Bezier interpolation */
    if ((prevbezt) && (prevbezt->ipo == BEZT_IPO_BEZ)) {

      ui::Layout &col = layout.column(true);
      uiItemL_respect_property_split(&col, IFACE_("Left Handle Type"), ICON_NONE);
      but = uiDefButR(block,
                      ui::ButtonType::Menu,
                      std::nullopt,
                      0,
                      0,
                      but_max_width,
                      UI_UNIT_Y,
                      &bezt_ptr,
                      "handle_left_type",
                      0,
                      0,
                      0,
                      "Type of left handle");
      button_retval_set(but, B_REDR);
      button_func_set(but, graphedit_activekey_handle_left_cb, fcu, bezt);

      uiItemL_respect_property_split(&col, IFACE_("Frame"), ICON_NONE);
      but = uiDefButR(block,
                      ui::ButtonType::Num,
                      "",
                      0,
                      0,
                      but_max_width,
                      UI_UNIT_Y,
                      &bezt_ptr,
                      "handle_left",
                      0,
                      0,
                      0,
                      std::nullopt);
      button_retval_set(but, B_REDR);
      button_func_set(but, graphedit_activekey_left_handle_coord_cb, fcu, bezt);

      uiItemL_respect_property_split(&col, IFACE_("Value"), ICON_NONE);
      but = uiDefButR(block,
                      ui::ButtonType::Num,
                      "",
                      0,
                      0,
                      but_max_width,
                      UI_UNIT_Y,
                      &bezt_ptr,
                      "handle_left",
                      1,
                      0,
                      0,
                      std::nullopt);
      button_retval_set(but, B_REDR);
      button_func_set(but, graphedit_activekey_left_handle_coord_cb, fcu, bezt);
      button_unit_type_set(but, unit);
    }

    /* next handle - only if current is Bezier interpolation */
    if (bezt->ipo == BEZT_IPO_BEZ) {
      /* NOTE: special update callbacks are needed on the coords here due to #39911 */

      ui::Layout &col = layout.column(true);
      uiItemL_respect_property_split(&col, IFACE_("Right Handle Type"), ICON_NONE);
      but = uiDefButR(block,
                      ui::ButtonType::Menu,
                      std::nullopt,
                      0,
                      0,
                      but_max_width,
                      UI_UNIT_Y,
                      &bezt_ptr,
                      "handle_right_type",
                      0,
                      0,
                      0,
                      "Type of right handle");
      button_retval_set(but, B_REDR);
      button_func_set(but, graphedit_activekey_handle_right_cb, fcu, bezt);

      uiItemL_respect_property_split(&col, IFACE_("Frame"), ICON_NONE);
      but = uiDefButR(block,
                      ui::ButtonType::Num,
                      "",
                      0,
                      0,
                      but_max_width,
                      UI_UNIT_Y,
                      &bezt_ptr,
                      "handle_right",
                      0,
                      0,
                      0,
                      std::nullopt);
      button_retval_set(but, B_REDR);
      button_func_set(but, graphedit_activekey_right_handle_coord_cb, fcu, bezt);

      uiItemL_respect_property_split(&col, IFACE_("Value"), ICON_NONE);
      but = uiDefButR(block,
                      ui::ButtonType::Num,
                      "",
                      0,
                      0,
                      but_max_width,
                      UI_UNIT_Y,
                      &bezt_ptr,
                      "handle_right",
                      1,
                      0,
                      0,
                      std::nullopt);
      button_retval_set(but, B_REDR);
      button_func_set(but, graphedit_activekey_right_handle_coord_cb, fcu, bezt);
      button_unit_type_set(but, unit);
    }
  }
  else {
    if ((fcu->bezt == nullptr) && (fcu->modifiers.first)) {
      /* modifiers only - so no keyframes to be active */
      layout.label(RPT_("F-Curve only has F-Modifiers"), ICON_NONE);
      layout.label(RPT_("See Modifiers panel below"), ICON_INFO);
    }
    else if (fcu->fpt) {
      /* samples only */
      layout.label(RPT_("F-Curve does not have any keyframes as it only contains sampled points"),
                   ICON_NONE);
    }
    else {
      layout.label(RPT_("No active keyframe on F-Curve"), ICON_NONE);
    }
  }

  MEM_delete(ale);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Drivers
 * \{ */

#define B_IPO_DEPCHANGE 10

static void do_graph_region_driver_buttons(bContext *C, void *id_v, int event)
{
  Main *bmain = CTX_data_main(C);
  Scene *scene = CTX_data_scene(C);

  switch (event) {
    case B_IPO_DEPCHANGE: {
/* Was not actually run ever (nullptr always passed as arg to this callback).
 * If needed again, will need to check how to pass both fcurve and ID... :/ */
#if 0
      /* force F-Curve & Driver to get re-evaluated (same as the old Update Dependencies) */
      FCurve *fcu = (FCurve *)fcu_v;
      ChannelDriver *driver = (fcu) ? fcu->driver : nullptr;

      /* clear invalid flags */
      if (fcu) {
        fcu->flag &= ~FCURVE_DISABLED;
        driver->flag &= ~DRIVER_FLAG_INVALID;
      }
#endif
      ID *id = static_cast<ID *>(id_v);
      AnimData *adt = BKE_animdata_from_id(id);

      /* Rebuild depsgraph for the new dependencies, and ensure evaluated copies get flushed. */
      DEG_relations_tag_update(bmain);
      DEG_id_tag_update_ex(bmain, id, ID_RECALC_SYNC_TO_EVAL);
      if (adt != nullptr) {
        if (adt->action != nullptr) {
          DEG_id_tag_update_ex(bmain, &adt->action->id, ID_RECALC_SYNC_TO_EVAL);
        }
        if (adt->tmpact != nullptr) {
          DEG_id_tag_update_ex(bmain, &adt->tmpact->id, ID_RECALC_SYNC_TO_EVAL);
        }
      }

      break;
    }
  }

  /* default for now */
  WM_event_add_notifier(C, NC_SCENE | ND_FRAME, scene); /* XXX could use better notifier */
}

/* callback to add a target variable to the active driver */
static void driver_add_var_cb(bContext *C, void *driver_v, void * /*arg*/)
{
  ChannelDriver *driver = static_cast<ChannelDriver *>(driver_v);

  /* add a new variable */
  driver_add_new_variable(driver);
  ED_undo_push(C, "Add Driver Variable");
}

/* callback to remove target variable from active driver */
static void driver_delete_var_cb(bContext *C, void *driver_v, void *dvar_v)
{
  ChannelDriver *driver = static_cast<ChannelDriver *>(driver_v);
  DriverVar *dvar = static_cast<DriverVar *>(dvar_v);

  /* remove the active variable */
  driver_free_variable_ex(driver, dvar);
  ED_undo_push(C, "Delete Driver Variable");
}

/* callback to report why a driver variable is invalid */
static void driver_dvar_invalid_name_query_cb(bContext *C, void *dvar_v, void * /*arg*/)
{
  ui::PopupMenu *pup = ui::popup_menu_begin(
      C, CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, "Invalid Variable Name"), ICON_NONE);
  ui::Layout &layout = *popup_menu_layout(pup);

  DriverVar *dvar = static_cast<DriverVar *>(dvar_v);

  if (dvar->flag & DVAR_FLAG_INVALID_EMPTY) {
    layout.label(RPT_("It cannot be left blank"), ICON_ERROR);
  }
  if (dvar->flag & DVAR_FLAG_INVALID_START_NUM) {
    layout.label(RPT_("It cannot start with a number"), ICON_ERROR);
  }
  if (dvar->flag & DVAR_FLAG_INVALID_START_CHAR) {
    layout.label(RPT_("It cannot start with a special character,"
                      " including '$', '@', '!', '~', '+', '-', '_', '.', or ' '"),
                 ICON_NONE);
  }
  if (dvar->flag & DVAR_FLAG_INVALID_HAS_SPACE) {
    layout.label(RPT_("It cannot contain spaces (e.g. 'a space')"), ICON_ERROR);
  }
  if (dvar->flag & DVAR_FLAG_INVALID_HAS_DOT) {
    layout.label(RPT_("It cannot contain dots (e.g. 'a.dot')"), ICON_ERROR);
  }
  if (dvar->flag & DVAR_FLAG_INVALID_HAS_SPECIAL) {
    layout.label(RPT_("It cannot contain special (non-alphabetical/numeric) characters"),
                 ICON_ERROR);
  }
  if (dvar->flag & DVAR_FLAG_INVALID_PY_KEYWORD) {
    layout.label(RPT_("It cannot be a reserved keyword in Python"), ICON_INFO);
  }

  popup_menu_end(C, pup);
}

/* callback to reset the driver's flags */
static void driver_update_flags_cb(bContext * /*C*/, void *fcu_v, void * /*arg*/)
{
  FCurve *fcu = static_cast<FCurve *>(fcu_v);
  ChannelDriver *driver = fcu->driver;

  /* clear invalid flags */
  fcu->flag &= ~FCURVE_DISABLED;
  driver->flag &= ~DRIVER_FLAG_INVALID;
}

/* drivers panel poll */
static bool graph_panel_drivers_poll(const bContext *C, PanelType * /*pt*/)
{
  SpaceGraph *sipo = CTX_wm_space_graph(C);

  if (sipo->mode != SIPO_MODE_DRIVERS) {
    return false;
  }

  return graph_panel_context(C, nullptr, nullptr);
}

static void graph_panel_driverVar_fallback(ui::Layout &layout,
                                           const DriverTarget *dtar,
                                           PointerRNA *dtar_ptr)
{
  if (dtar->options & DTAR_OPTION_USE_FALLBACK) {
    ui::Layout &row = layout.row(true);
    row.prop(dtar_ptr, "use_fallback_value", UI_ITEM_NONE, "", ICON_NONE);
    row.prop(dtar_ptr, "fallback_value", UI_ITEM_NONE, std::nullopt, ICON_NONE);
  }
  else {
    layout.prop(dtar_ptr, "use_fallback_value", UI_ITEM_NONE, std::nullopt, ICON_NONE);
  }
}

/* settings for 'single property' driver variable type */
static void graph_panel_driverVar__singleProp(ui::Layout &layout, ID *id, DriverVar *dvar)
{
  DriverTarget *dtar = &dvar->targets[0];

  /* initialize RNA pointer to the target */
  PointerRNA dtar_ptr = RNA_pointer_create_discrete(id, RNA_DriverTarget, dtar);

  /* Target ID */
  ui::Layout &row = layout.row(false);
  row.red_alert_set((dtar->flag & DTAR_FLAG_INVALID) && !dtar->id);
  template_any_id(&row, &dtar_ptr, "id", "id_type", IFACE_("Prop:"));

  /* Target Property */
  if (dtar->id) {
    /* get pointer for resolving the property selected */
    PointerRNA root_ptr = RNA_id_pointer_create(dtar->id);

    /* rna path */
    ui::Layout &col = layout.column(true);
    col.red_alert_set(dtar->flag & (DTAR_FLAG_INVALID | DTAR_FLAG_FALLBACK_USED));
    template_path_builder(&col,
                          &dtar_ptr,
                          "data_path",
                          &root_ptr,
                          CTX_IFACE_(BLT_I18NCONTEXT_EDITOR_FILEBROWSER, "Path"));

    /* Default value. */
    graph_panel_driverVar_fallback(layout, dtar, &dtar_ptr);
  }
}

/* settings for 'rotation difference' driver variable type */
/* FIXME: 1) Must be same armature for both dtars, 2) Alignment issues... */
static void graph_panel_driverVar__rotDiff(ui::Layout &layout, ID *id, DriverVar *dvar)
{
  for (int i : IndexRange(2)) {
    DriverTarget *dtar = &dvar->targets[i];
    /* Initialize RNA pointer to the target */
    PointerRNA ptr = RNA_pointer_create_discrete(id, RNA_DriverTarget, dtar);

    ui::Layout &col = layout.column(true);
    col.red_alert_set(dtar->flag & DTAR_FLAG_INVALID); /* XXX: per field... */
    StringRef name = i == 1 ? IFACE_("Object 1") : IFACE_("Object 2");
    col.prop(&ptr, "id", UI_ITEM_NONE, name, ICON_NONE);

    Object *ob = id_cast<Object *>(dtar->id);
    if (dtar->id && GS(dtar->id->name) == ID_OB && ob->pose) {
      PointerRNA tar_ptr = RNA_pointer_create_discrete(dtar->id, RNA_Pose, ob->pose);
      col.prop_search(&ptr, "bone_target", &tar_ptr, "bones", "", ICON_BONE_DATA);
    }
  }
}

/* settings for 'location difference' driver variable type */

static void graph_panel_driverVar__locDiff(ui::Layout &layout, ID *id, DriverVar *dvar)
{

  for (int i : IndexRange(2)) {
    DriverTarget *dtar = &dvar->targets[i];
    /* Initialize RNA pointer to the target */
    PointerRNA ptr = RNA_pointer_create_discrete(id, RNA_DriverTarget, dtar);

    ui::Layout &col = layout.column(true);
    col.red_alert_set(dtar->flag & DTAR_FLAG_INVALID); /* XXX: per field... */
    StringRef name = i == 1 ? IFACE_("Object 1") : IFACE_("Object 2");
    col.prop(&ptr, "id", UI_ITEM_NONE, name, ICON_NONE);

    Object *ob = id_cast<Object *>(dtar->id);

    if (dtar->id && GS(dtar->id->name) == ID_OB && ob->pose) {
      PointerRNA tar_ptr = RNA_pointer_create_discrete(dtar->id, RNA_Pose, ob->pose);
      col.prop_search(&ptr, "bone_target", &tar_ptr, "bones", IFACE_("Bone"), ICON_BONE_DATA);
    }

    /* We can clear it again now - it's only needed when creating the ID/Bone fields */
    col.red_alert_set(false);

    col.prop(&ptr, "transform_space", UI_ITEM_NONE, std::nullopt, ICON_NONE);
  }
}

/* settings for 'transform channel' driver variable type */
static void graph_panel_driverVar__transChan(ui::Layout &layout, ID *id, DriverVar *dvar)
{
  DriverTarget *dtar = &dvar->targets[0];
  Object *ob = id_cast<Object *>(dtar->id);

  /* initialize RNA pointer to the target */
  PointerRNA dtar_ptr = RNA_pointer_create_discrete(id, RNA_DriverTarget, dtar);

  /* properties */
  ui::Layout &col = layout.column(true);
  col.red_alert_set(dtar->flag & DTAR_FLAG_INVALID); /* XXX: per field... */
  col.prop(&dtar_ptr, "id", UI_ITEM_NONE, IFACE_("Object"), ICON_NONE);

  if (dtar->id && GS(dtar->id->name) == ID_OB && ob->pose) {
    PointerRNA tar_ptr = RNA_pointer_create_discrete(dtar->id, RNA_Pose, ob->pose);
    col.prop_search(&dtar_ptr, "bone_target", &tar_ptr, "bones", IFACE_("Bone"), ICON_BONE_DATA);
  }

  ui::Layout &sub = layout.column(true);
  sub.prop(&dtar_ptr, "transform_type", UI_ITEM_NONE, std::nullopt, ICON_NONE);

  if (ELEM(dtar->transChan,
           DTAR_TRANSCHAN_ROTX,
           DTAR_TRANSCHAN_ROTY,
           DTAR_TRANSCHAN_ROTZ,
           DTAR_TRANSCHAN_ROTW))
  {
    sub.prop(&dtar_ptr, "rotation_mode", UI_ITEM_NONE, IFACE_("Mode"), ICON_NONE);
  }

  sub.prop(&dtar_ptr, "transform_space", UI_ITEM_NONE, IFACE_("Space"), ICON_NONE);
}

/* Settings for 'Context Property' driver variable type. */
static void graph_panel_driverVar__contextProp(ui::Layout &layout, ID *id, DriverVar *dvar)
{
  DriverTarget *dtar = &dvar->targets[0];

  /* Initialize RNA pointer to the target. */
  PointerRNA dtar_ptr = RNA_pointer_create_discrete(id, RNA_DriverTarget, dtar);

  /* Target Property. */
  {
    ui::Layout &row = layout.row(false);
    row.prop(&dtar_ptr, "context_property", UI_ITEM_NONE, std::nullopt, ICON_NONE);
  }

  /* Target Path */
  {
    ui::Layout &col = layout.column(true);
    col.red_alert_set(dtar->flag & (DTAR_FLAG_INVALID | DTAR_FLAG_FALLBACK_USED));
    template_path_builder(&col,
                          &dtar_ptr,
                          "data_path",
                          nullptr,
                          CTX_IFACE_(BLT_I18NCONTEXT_EDITOR_FILEBROWSER, "Path"));
  }

  /* Default value. */
  graph_panel_driverVar_fallback(layout, dtar, &dtar_ptr);
}

/* ----------------------------------------------------------------- */

/* property driven by the driver - duplicates Active FCurve, but useful for clarity */

static void graph_draw_driven_property_enabled_btn(ui::Layout &layout,
                                                   ID *id,
                                                   FCurve *fcu,
                                                   const char *label)
{
  PointerRNA fcurve_ptr = RNA_pointer_create_discrete(id, RNA_FCurve, fcu);

  ui::Block *block = layout.block();
  uiDefButR(block,
            ui::ButtonType::CheckboxN,
            label,
            0,
            0,
            UI_UNIT_X,
            UI_UNIT_Y,
            &fcurve_ptr,
            "mute",
            0,
            0,
            0,
            TIP_("Let the driver determine this property's value"));
}

static void graph_panel_drivers_header(const bContext *C, Panel *panel)
{
  bAnimListElem *ale;
  FCurve *fcu;
  if (!graph_panel_context(C, &ale, &fcu)) {
    return;
  }

  graph_draw_driven_property_enabled_btn(*panel->layout, ale->id, fcu, IFACE_("Driver"));
  MEM_delete(ale);
}

static void graph_draw_driven_property_panel(ui::Layout &layout, ID *id, FCurve *fcu)
{
  char name[256];
  int icon = 0;

  /* get user-friendly 'name' for F-Curve */
  const std::optional<int> optional_icon = getname_anim_fcurve(name, id, fcu);
  if (optional_icon) {
    icon = *optional_icon;
  }
  else {
    icon = RNA_struct_ui_icon(ID_code_to_RNA_type(GS(id->name)));
  }

  /* panel layout... */
  ui::Layout &row = layout.row(true);
  row.alignment_set(ui::LayoutAlign::Left);

  /* -> user friendly 'name' for datablock that owns F-Curve */
  /* XXX: Actually, we may need the datablock icons only...
   * (e.g. right now will show bone for bone props). */
  row.label(id->name + 2, icon);

  /* -> user friendly 'name' for F-Curve/driver target */
  row.label("", ICON_RIGHTARROW);
  row.label(name, ICON_RNA);
}

/* UI properties panel layout for driver settings - shared for Drivers Editor and for */
static void graph_draw_driver_settings_panel(ui::Layout &layout,
                                             ID *id,
                                             FCurve *fcu,
                                             const bool is_popover)
{
  ChannelDriver *driver = fcu->driver;
  ui::Block *block = layout.block();

  ui::Button *but;

  /* set event handler for panel */
  block_func_handle_set(block, do_graph_region_driver_buttons, id);

  /* driver-level settings - type, expressions, and errors */
  PointerRNA driver_ptr = RNA_pointer_create_discrete(id, RNA_Driver, driver);

  {
    ui::Layout &col = layout.column(true);
    col.prop(&driver_ptr, "type", UI_ITEM_NONE, std::nullopt, ICON_NONE);

    char valBuf[32];

    /* value of driver */
    ui::Layout &row = col.row(true);
    row.label(IFACE_("Driver Value:"), ICON_NONE);
    SNPRINTF_UTF8(valBuf, "%.3f", driver->curval);
    row.label(valBuf, ICON_NONE);
  }

  layout.separator();
  layout.separator();

  /* show expression box if doing scripted drivers,
   * and/or error messages when invalid drivers exist */
  if (driver->type == DRIVER_TYPE_PYTHON) {
    bool bpy_data_expr_error = (strstr(driver->expression, "bpy.data.") != nullptr);
    bool bpy_ctx_expr_error = (strstr(driver->expression, "bpy.context.") != nullptr);

    /* expression */
    /* TODO: "Show syntax hints" button */
    ui::Layout &expr_col = layout.column(true);

    expr_col.label(IFACE_("Expression:"), ICON_NONE);
    expr_col.prop(&driver_ptr, "expression", UI_ITEM_NONE, "", ICON_NONE);
    expr_col.prop(&driver_ptr, "use_self", UI_ITEM_NONE, std::nullopt, ICON_NONE);

    /* errors? */
    ui::Layout &error_col = layout.column(true);

    if (driver->flag & DRIVER_FLAG_PYTHON_BLOCKED) {
      /* TODO: Add button to enable? */
      error_col.label(RPT_("Python restricted for security"), ICON_ERROR);
      error_col.label(RPT_("Slow Python expression"), ICON_INFO);
    }
    else if (driver->flag & DRIVER_FLAG_INVALID) {
      error_col.label(RPT_("ERROR: Invalid Python expression"), ICON_CANCEL);
    }
    else if (!BKE_driver_has_simple_expression(driver)) {
      error_col.label(RPT_("Slow Python expression"), ICON_INFO);
    }

    /* Explicit bpy-references are evil. Warn about these to prevent errors */
    /* TODO: put these in a box? */
    if (bpy_data_expr_error || bpy_ctx_expr_error) {
      error_col.label(RPT_("WARNING: Driver expression may not work correctly"), ICON_HELP);

      if (bpy_data_expr_error) {
        error_col.label(RPT_("TIP: Use variables instead of bpy.data paths (see below)"),
                        ICON_ERROR);
      }
      if (bpy_ctx_expr_error) {
        error_col.label(RPT_("TIP: bpy.context is not safe for renderfarm usage"), ICON_ERROR);
      }
    }
  }
  else {
    /* errors? */
    ui::Layout &col = layout.column(true);

    if (driver->flag & DRIVER_FLAG_INVALID) {
      col.label(RPT_("ERROR: Invalid target channel(s)"), ICON_ERROR);
    }

    /* Warnings about a lack of variables
     * NOTE: The lack of variables is generally a bad thing, since it indicates
     *       that the driver doesn't work at all. This particular scenario arises
     *       primarily when users mistakenly try to use drivers for procedural
     *       property animation
     */
    if (driver->variables.is_empty()) {
      col.label(RPT_("ERROR: Driver is useless without any inputs"), ICON_ERROR);

      if (!fcu->modifiers.is_empty()) {
        col.label(RPT_("TIP: Use F-Curves for procedural animation instead"), ICON_INFO);
        col.label(RPT_("F-Modifiers can generate curves for those too"), ICON_INFO);
      }
    }
  }

  layout.separator();

  /* add/copy/paste driver variables */
  ui::Layout &row = layout.row(false);

  {
    /* Add driver variable - add blank. */
    ui::Layout &sub = row.row(true);
    but = uiDefIconTextBut(
        block,
        ui::ButtonType::But,
        ICON_ADD,
        IFACE_("Add Input Variable"),
        0,
        0,
        10 * UI_UNIT_X,
        UI_UNIT_Y,
        nullptr,
        TIP_("Add a Driver Variable to keep track of an input used by the driver"));
    button_retval_set(but, B_IPO_DEPCHANGE);
    button_func_set(but, driver_add_var_cb, driver, nullptr);

    if (is_popover) {
      /* add driver variable - add using eyedropper */
      /* XXX: will this operator work like this? */
      sub.op("UI_OT_eyedropper_driver", "", ICON_EYEDROPPER);
    }
  }

  {
    /* copy/paste (as sub-row) */
    ui::Layout &sub = row.row(true);

    sub.op("GRAPH_OT_driver_variables_copy", "", ICON_COPYDOWN);
    sub.op("GRAPH_OT_driver_variables_paste", "", ICON_PASTEDOWN);
  }

  /* loop over targets, drawing them */
  for (DriverVar &dvar : driver->variables) {

    /* sub-layout column for this variable's settings */
    ui::Layout &col = layout.column(true);

    /* 1) header panel */
    ui::Layout &header_box = col.box();
    PointerRNA dvar_ptr = RNA_pointer_create_discrete(id, RNA_DriverVariable, &dvar);

    /* 1.1) variable type and name */
    ui::Layout &sub = header_box.row(false).row(true);

    /* 1.1.1) variable type */

    /* HACK: special group just for the enum,
     * otherwise we get ugly layout with text included too... */
    ui::Layout &type_sub = sub.row(true);

    type_sub.alignment_set(ui::LayoutAlign::Left);

    type_sub.prop(&dvar_ptr, "type", ui::ITEM_R_ICON_ONLY, "", ICON_NONE);

    /* 1.1.2) variable name */

    /* HACK: special group to counteract the effects of the previous enum,
     * which now pushes everything too far right */
    ui::Layout &name_sub = sub.row(true);

    name_sub.alignment_set(ui::LayoutAlign::Expand);

    name_sub.prop(&dvar_ptr, "name", UI_ITEM_NONE, "", ICON_NONE);

    /* 1.2) invalid name? */
    block_emboss_set(block, ui::EmbossType::None);

    if (dvar.flag & DVAR_FLAG_INVALID_NAME) {
      but = uiDefIconBut(block,
                         ui::ButtonType::But,
                         ICON_ERROR,
                         290,
                         0,
                         UI_UNIT_X,
                         UI_UNIT_Y,
                         nullptr,
                         0.0,
                         0.0,
                         TIP_("Invalid variable name, click here for details"));
      button_retval_set(but, B_IPO_DEPCHANGE);
      button_func_set(but, driver_dvar_invalid_name_query_cb, &dvar, nullptr); /* XXX: reports? */
    }

    /* 1.3) remove button */
    but = uiDefIconBut(block,
                       ui::ButtonType::But,
                       ICON_X,
                       290,
                       0,
                       UI_UNIT_X,
                       UI_UNIT_Y,
                       nullptr,
                       0.0,
                       0.0,
                       TIP_("Delete target variable"));
    button_retval_set(but, B_IPO_DEPCHANGE);
    button_func_set(but, driver_delete_var_cb, driver, &dvar);
    block_emboss_set(block, ui::EmbossType::Emboss);

    /* 2) variable type settings */
    ui::Layout &box = col.box();
    /* controls to draw depends on the type of variable */
    switch (dvar.type) {
      case DVAR_TYPE_SINGLE_PROP: /* single property */
        graph_panel_driverVar__singleProp(box, id, &dvar);
        break;
      case DVAR_TYPE_ROT_DIFF: /* rotational difference */
        graph_panel_driverVar__rotDiff(box, id, &dvar);
        break;
      case DVAR_TYPE_LOC_DIFF: /* location difference */
        graph_panel_driverVar__locDiff(box, id, &dvar);
        break;
      case DVAR_TYPE_TRANSFORM_CHAN: /* transform channel */
        graph_panel_driverVar__transChan(box, id, &dvar);
        break;
      case DVAR_TYPE_CONTEXT_PROP: /* context property */
        graph_panel_driverVar__contextProp(box, id, &dvar);
        break;
      case MAX_DVAR_TYPES:
        break;
    }

    /* 3) value of variable */
    {
      char valBuf[32];

      ui::Layout &sub = col.box().row(true);
      sub.label(IFACE_("Value:"), ICON_NONE);

      if ((dvar.type == DVAR_TYPE_ROT_DIFF) ||
          (dvar.type == DVAR_TYPE_TRANSFORM_CHAN &&
           ELEM(dvar.targets[0].transChan,
                DTAR_TRANSCHAN_ROTX,
                DTAR_TRANSCHAN_ROTY,
                DTAR_TRANSCHAN_ROTZ,
                DTAR_TRANSCHAN_ROTW) &&
           dvar.targets[0].rotation_mode != DTAR_ROTMODE_QUATERNION))
      {
        SNPRINTF_UTF8(valBuf,
                      "%.3f (%4.1f" BLI_STR_UTF8_DEGREE_SIGN ")",
                      dvar.curval,
                      RAD2DEGF(dvar.curval));
      }
      else {
        SNPRINTF_UTF8(valBuf, "%.3f", dvar.curval);
      }

      sub.label(valBuf, ICON_NONE);
    }
  }

  layout.separator();
  layout.separator();

  /* XXX: This should become redundant. But sometimes the flushing fails,
   * so keep this around for a while longer as a "last resort" */
  layout.row(true);
  but = uiDefIconTextBut(block,
                         ui::ButtonType::But,
                         ICON_FILE_REFRESH,
                         IFACE_("Update Dependencies"),
                         0,
                         0,
                         10 * UI_UNIT_X,
                         UI_UNIT_Y,
                         nullptr,
                         TIP_("Force updates of dependencies - Only use this if drivers are not "
                              "updating correctly"));
  button_retval_set(but, B_IPO_DEPCHANGE);
  button_func_set(but, driver_update_flags_cb, fcu, nullptr);
}

/* ----------------------------------------------------------------- */

/* Panel to show property driven by the driver (in Drivers Editor) - duplicates Active FCurve,
 * but useful for clarity. */
static void graph_panel_driven_property(const bContext *C, Panel *panel)
{
  bAnimListElem *ale;
  FCurve *fcu;

  if (!graph_panel_context(C, &ale, &fcu)) {
    return;
  }

  graph_draw_driven_property_panel(*panel->layout, ale->id, fcu);

  MEM_delete(ale);
}

/* driver settings for active F-Curve
 * (only for 'Drivers' mode in Graph Editor, i.e. the full "Drivers Editor") */
static void graph_panel_drivers(const bContext *C, Panel *panel)
{
  bAnimListElem *ale;
  FCurve *fcu;

  /* Get settings from context */
  if (!graph_panel_context(C, &ale, &fcu)) {
    return;
  }

  graph_draw_driver_settings_panel(*panel->layout, ale->id, fcu, false);

  /* cleanup */
  MEM_delete(ale);
}

/* ----------------------------------------------------------------- */

/* Poll to make this not show up in the graph editor,
 * as this is only to be used as a popup elsewhere. */
static bool graph_panel_drivers_popover_poll(const bContext *C, PanelType * /*pt*/)
{
  return ED_operator_graphedit_active(const_cast<bContext *>(C)) == false;
}

/* popover panel for driver editing anywhere in ui */
static void graph_panel_drivers_popover(const bContext *C, Panel *panel)
{
  ui::Layout &layout = *panel->layout;

  PointerRNA ptr = {};
  PropertyRNA *prop = nullptr;
  int index = -1;
  ui::Button *but = nullptr;

  /* Get active property to show driver properties for */
  but = ui::region_active_but_prop_get(CTX_wm_region(C), &ptr, &prop, &index);
  if (but) {
    FCurve *fcu;
    bool driven, special;

    fcu = BKE_fcurve_find_by_rna_context_ui(
        const_cast<bContext *>(C), &ptr, prop, index, nullptr, nullptr, &driven, &special);

    /* Hack: Force all buttons in this panel to be able to know the driver button
     * this panel is getting spawned from, so that things like the "Open Drivers Editor"
     * button will work.
     */
    layout.context_set_from_but(but);

    /* Populate Panel - With a combination of the contents of the Driven and Driver panels */
    if (fcu && fcu->driver) {
      ID *id = ptr.owner_id;

      PointerRNA ptr_fcurve = RNA_pointer_create_discrete(id, RNA_FCurve, fcu);
      layout.context_ptr_set("active_editable_fcurve", &ptr_fcurve);

      /* Driven Property Settings */
      layout.label(IFACE_("Driven Property:"), ICON_NONE);
      graph_draw_driven_property_panel(*panel->layout, id, fcu);
      /* TODO: All vs Single */

      layout.separator();
      layout.separator();

      /* Drivers Settings */
      graph_draw_driven_property_enabled_btn(*panel->layout, id, fcu, IFACE_("Driver:"));
      graph_draw_driver_settings_panel(*panel->layout, id, fcu, true);
    }
  }

  /* Show drivers editor is always visible */
  layout.op("SCREEN_OT_drivers_editor_show", IFACE_("Show in Drivers Editor"), ICON_DRIVER);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name F-Curve Modifiers
 *
 * \note All the drawing code is in `editors/animation/fmodifier_ui.cc`.
 * \{ */

#define B_FMODIFIER_REDRAW 20
/** The start of FModifier panels registered for the graph editor. */
#define GRAPH_FMODIFIER_PANEL_PREFIX "GRAPH"

static void graph_fmodifier_panel_id(void *fcm_link, char *r_name)
{
  FModifier *fcm = static_cast<FModifier *>(fcm_link);
  eFModifier_Types type = eFModifier_Types(fcm->type);
  const FModifierTypeInfo *fmi = get_fmodifier_typeinfo(type);

  if (!fmi) {
    /* This can happen when the blend file has data for a modifier that doesn't exist in this
     * Blender version (when the blend file is newer). */
    r_name[0] = '\0';
    return;
  }

  BLI_snprintf_utf8(r_name, BKE_ST_MAXNAME, "%s_PT_%s", GRAPH_FMODIFIER_PANEL_PREFIX, fmi->name);
}

static void do_graph_region_modifier_buttons(bContext *C, void * /*arg*/, int event)
{
  switch (event) {
    case B_FMODIFIER_REDRAW: /* XXX this should send depsgraph updates too */
      /* XXX: need a notifier specially for F-Modifiers */
      WM_event_add_notifier(C, NC_ANIMATION, nullptr);
      break;
  }
}

static void graph_panel_modifiers(const bContext *C, Panel *panel)
{
  bAnimListElem *ale;
  FCurve *fcu;

  if (!graph_panel_context(C, &ale, &fcu)) {
    return;
  }

  ui::Block *block = panel->layout->block();
  block_func_handle_set(block, do_graph_region_modifier_buttons, nullptr);

  /* 'add modifier' button at top of panel */
  {
    ui::Layout &row = panel->layout->row(false);

    /* this is an operator button which calls a 'add modifier' operator...
     * a menu might be nicer but would be tricky as we need some custom filtering
     */
    row.op_menu_enum(C, "GRAPH_OT_fmodifier_add", "type", IFACE_("Add Modifier"), ICON_NONE);

    /* copy/paste (as sub-row) */
    ui::Layout &sub = row.row(true);
    sub.op("GRAPH_OT_fmodifier_copy", "", ICON_COPYDOWN);
    sub.op("GRAPH_OT_fmodifier_paste", "", ICON_PASTEDOWN);
  }

  ANIM_fmodifier_panels(C, ale->fcurve_owner_id, &fcu->modifiers, graph_fmodifier_panel_id);

  MEM_delete(ale);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Registration
 * \{ */

void graph_buttons_register(ARegionType *art)
{
  PanelType *pt;

  pt = MEM_new_zeroed<PanelType>("spacetype graph panel properties");
  STRNCPY_UTF8(pt->idname, "GRAPH_PT_properties");
  STRNCPY_UTF8(pt->label, N_("Active F-Curve"));
  STRNCPY_UTF8(pt->category, "F-Curve");
  STRNCPY_UTF8(pt->translation_context, BLT_I18NCONTEXT_DEFAULT_BPYRNA);
  pt->draw = graph_panel_properties;
  pt->poll = graph_panel_poll;
  BLI_addtail(&art->paneltypes, pt);

  pt = MEM_new_zeroed<PanelType>("spacetype graph panel properties");
  STRNCPY_UTF8(pt->idname, "GRAPH_PT_key_properties");
  STRNCPY_UTF8(pt->label, N_("Active Keyframe"));
  STRNCPY_UTF8(pt->category, "F-Curve");
  STRNCPY_UTF8(pt->translation_context, BLT_I18NCONTEXT_DEFAULT_BPYRNA);
  pt->draw = graph_panel_key_properties;
  pt->poll = graph_panel_poll;
  BLI_addtail(&art->paneltypes, pt);

  pt = MEM_new_zeroed<PanelType>("spacetype graph panel drivers driven");
  STRNCPY_UTF8(pt->idname, "GRAPH_PT_driven_property");
  STRNCPY_UTF8(pt->label, N_("Driven Property"));
  STRNCPY_UTF8(pt->category, "Drivers");
  STRNCPY_UTF8(pt->translation_context, BLT_I18NCONTEXT_DEFAULT_BPYRNA);
  pt->draw = graph_panel_driven_property;
  pt->poll = graph_panel_drivers_poll;
  BLI_addtail(&art->paneltypes, pt);

  pt = MEM_new_zeroed<PanelType>("spacetype graph panel drivers");
  STRNCPY_UTF8(pt->idname, "GRAPH_PT_drivers");
  STRNCPY_UTF8(pt->label, N_("Driver"));
  STRNCPY_UTF8(pt->category, "Drivers");
  STRNCPY_UTF8(pt->translation_context, BLT_I18NCONTEXT_DEFAULT_BPYRNA);
  pt->draw = graph_panel_drivers;
  pt->draw_header = graph_panel_drivers_header;
  pt->poll = graph_panel_drivers_poll;
  BLI_addtail(&art->paneltypes, pt);

  pt = MEM_new_zeroed<PanelType>("spacetype graph panel drivers popover");
  STRNCPY_UTF8(pt->idname, "GRAPH_PT_drivers_popover");
  STRNCPY_UTF8(pt->label, N_("Add/Edit Driver"));
  STRNCPY_UTF8(pt->category, "Drivers");
  STRNCPY_UTF8(pt->translation_context, BLT_I18NCONTEXT_DEFAULT_BPYRNA);
  pt->draw = graph_panel_drivers_popover;
  pt->poll = graph_panel_drivers_popover_poll;
  BLI_addtail(&art->paneltypes, pt);
  /* This panel isn't used in this region.
   * Add explicitly to global list (so popovers work). */
  WM_paneltype_add(pt);

  pt = MEM_new_zeroed<PanelType>("spacetype graph panel modifiers");
  STRNCPY_UTF8(pt->idname, "GRAPH_PT_modifiers");
  STRNCPY_UTF8(pt->label, N_("Modifiers"));
  STRNCPY_UTF8(pt->category, "Modifiers");
  STRNCPY_UTF8(pt->translation_context, BLT_I18NCONTEXT_DEFAULT_BPYRNA);
  pt->flag = PANEL_TYPE_NO_HEADER;
  pt->draw = graph_panel_modifiers;
  pt->poll = graph_panel_poll;
  BLI_addtail(&art->paneltypes, pt);

  ANIM_modifier_panels_register_graph_and_NLA(art, GRAPH_FMODIFIER_PANEL_PREFIX, graph_panel_poll);
  ANIM_modifier_panels_register_graph_only(art, GRAPH_FMODIFIER_PANEL_PREFIX, graph_panel_poll);

  pt = MEM_new_zeroed<PanelType>("spacetype graph panel view");
  STRNCPY_UTF8(pt->idname, "GRAPH_PT_view");
  STRNCPY_UTF8(pt->label, N_("Show Cursor"));
  STRNCPY_UTF8(pt->category, "View");
  STRNCPY_UTF8(pt->translation_context, BLT_I18NCONTEXT_DEFAULT_BPYRNA);
  pt->draw = graph_panel_cursor;
  pt->draw_header = graph_panel_cursor_header;
  BLI_addtail(&art->paneltypes, pt);
}

/** \} */

}  // namespace blender
