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

/** \file
 * \ingroup wm
 *
 * Tool-system used to define tools in Blender's toolbar.
 * See: `./scripts/startup/bl_ui/space_toolsystem_common.py`, `ToolDef` for a detailed
 * description of tool definitions.
 *
 * \note Tools are stored per workspace.
 * Notice many functions take #Main & #WorkSpace and *not* window/screen/scene data.
 * This is intentional as changing tools must account for all scenes using that workspace.
 * Functions that refreshes on tool change are responsible for updating all windows using
 * this workspace.
 */

#include <cstring>

#include "CLG_log.h"

#include "MEM_guardedalloc.h"

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

#include "DNA_ID.h"
#include "DNA_brush_types.h"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "DNA_space_types.h"
#include "DNA_windowmanager_types.h"
#include "DNA_workspace_types.h"

#include "BKE_asset_edit.hh"
#include "BKE_brush.hh"
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_idprop.hh"
#include "BKE_layer.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_paint.hh"
#include "BKE_workspace.hh"

#include "RNA_access.hh"
#include "RNA_enum_types.hh"

#include "WM_api.hh"
#include "WM_message.hh"
#include "WM_toolsystem.hh" /* Own include. */
#include "WM_types.hh"

static void toolsystem_reinit_with_toolref(bContext *C, WorkSpace * /*workspace*/, bToolRef *tref);
static bToolRef *toolsystem_reinit_ensure_toolref(bContext *C,
                                                  WorkSpace *workspace,
                                                  const bToolKey *tkey,
                                                  const char *default_tool);
static void toolsystem_refresh_screen_from_active_tool(Main *bmain,
                                                       WorkSpace *workspace,
                                                       bToolRef *tref);
static void toolsystem_ref_set_by_brush_type(bContext *C, const char *brush_type);

static void toolsystem_ref_set_by_id_pending(Main *bmain,
                                             bToolRef *tref,
                                             const char *idname_pending);

/* -------------------------------------------------------------------- */
/** \name Tool Reference API
 * \{ */

bToolRef *WM_toolsystem_ref_from_context(const bContext *C)
{
  WorkSpace *workspace = CTX_wm_workspace(C);
  if (workspace == nullptr) {
    return nullptr;
  }
  const Scene *scene = CTX_data_scene(C);
  ViewLayer *view_layer = CTX_data_view_layer(C);
  ScrArea *area = CTX_wm_area(C);
  if ((area == nullptr) || ((1 << area->spacetype) & WM_TOOLSYSTEM_SPACE_MASK) == 0) {
    return nullptr;
  }
  bToolKey tkey{};
  tkey.space_type = area->spacetype;
  tkey.mode = WM_toolsystem_mode_from_spacetype(scene, view_layer, area, area->spacetype);
  bToolRef *tref = WM_toolsystem_ref_find(workspace, &tkey);
  /* We could return 'area->runtime.tool' in this case. */
  if (area->runtime.is_tool_set) {
    BLI_assert(tref == area->runtime.tool);
  }
  return tref;
}

bToolRef_Runtime *WM_toolsystem_runtime_from_context(const bContext *C)
{
  bToolRef *tref = WM_toolsystem_ref_from_context(C);
  return tref ? tref->runtime : nullptr;
}

bToolRef *WM_toolsystem_ref_find(WorkSpace *workspace, const bToolKey *tkey)
{
  BLI_assert((1 << tkey->space_type) & WM_TOOLSYSTEM_SPACE_MASK);
  LISTBASE_FOREACH (bToolRef *, tref, &workspace->tools) {
    if ((tref->space_type == tkey->space_type) && (tref->mode == tkey->mode)) {
      return tref;
    }
  }
  return nullptr;
}

bToolRef_Runtime *WM_toolsystem_runtime_find(WorkSpace *workspace, const bToolKey *tkey)
{
  bToolRef *tref = WM_toolsystem_ref_find(workspace, tkey);
  return tref ? tref->runtime : nullptr;
}

bool WM_toolsystem_ref_ensure(WorkSpace *workspace, const bToolKey *tkey, bToolRef **r_tref)
{
  bToolRef *tref = WM_toolsystem_ref_find(workspace, tkey);
  if (tref) {
    *r_tref = tref;
    return false;
  }
  tref = MEM_callocN<bToolRef>(__func__);
  BLI_addhead(&workspace->tools, tref);
  tref->space_type = tkey->space_type;
  tref->mode = tkey->mode;
  *r_tref = tref;
  return true;
}

/**
 * Similar to #toolsystem_active_tool_from_context_or_view3d(), but returns the tool key only.
 */
static bToolKey toolsystem_key_from_context_or_view3d(const Scene *scene,
                                                      ViewLayer *view_layer,
                                                      ScrArea *area)
{
  bToolKey tkey{};

  if (area && ((1 << area->spacetype) & WM_TOOLSYSTEM_SPACE_MASK)) {
    tkey.space_type = area->spacetype;
    tkey.mode = WM_toolsystem_mode_from_spacetype(scene, view_layer, area, area->spacetype);
    return tkey;
  }

  /* Otherwise: Fallback to getting the active tool for 3D views. */
  tkey.space_type = SPACE_VIEW3D;
  tkey.mode = WM_toolsystem_mode_from_spacetype(scene, view_layer, nullptr, SPACE_VIEW3D);
  return tkey;
}

/**
 * Get the active tool for the current context (space and mode) if the current space supports tools
 * or, fallback to the active tool of the 3D View in the current mode.
 *
 * Use this instead of #WM_toolsystem_ref_from_context() when usage from properties editors should
 * be possible, which shows tool settings of the 3D View.
 */
static const bToolRef *toolsystem_active_tool_from_context_or_view3d(const bContext *C)
{
  /* Current space & mode has its own active tool, use that. */
  const ScrArea *area = CTX_wm_area(C);
  if (area && ((1 << area->spacetype) & WM_TOOLSYSTEM_SPACE_MASK)) {
    return WM_toolsystem_ref_from_context(C);
  }

  /* Otherwise: Fallback to getting the active tool for 3D views. */
  WorkSpace *workspace = CTX_wm_workspace(C);
  if (workspace == nullptr) {
    return nullptr;
  }
  const Scene *scene = CTX_data_scene(C);
  ViewLayer *view_layer = CTX_data_view_layer(C);
  bToolKey tkey{};
  tkey.space_type = SPACE_VIEW3D;
  tkey.mode = WM_toolsystem_mode_from_spacetype(scene, view_layer, nullptr, SPACE_VIEW3D);
  return WM_toolsystem_ref_find(workspace, &tkey);
}

/** \} */

static void toolsystem_unlink_ref(bContext *C, WorkSpace * /*workspace*/, bToolRef *tref)
{
  bToolRef_Runtime *tref_rt = tref->runtime;

  if (tref_rt->gizmo_group[0]) {
    wmGizmoGroupType *gzgt = WM_gizmogrouptype_find(tref_rt->gizmo_group, false);
    if (gzgt != nullptr) {
      Main *bmain = CTX_data_main(C);
      WM_gizmo_group_remove_by_tool(C, bmain, gzgt, tref);
    }
  }
}
void WM_toolsystem_unlink(bContext *C, WorkSpace *workspace, const bToolKey *tkey)
{
  bToolRef *tref = WM_toolsystem_ref_find(workspace, tkey);
  if (tref && tref->runtime) {
    toolsystem_unlink_ref(C, workspace, tref);
  }
}

/* -------------------------------------------------------------------- */
/** \name Brush Tools
 * \{ */

static const char *brush_type_identifier_get(const int brush_type, const PaintMode paint_mode)
{
  const EnumPropertyItem *type_enum = BKE_paint_get_tool_enum_from_paintmode(paint_mode);
  const int item_idx = RNA_enum_from_value(type_enum, brush_type);
  if (item_idx == -1) {
    return "";
  }
  return type_enum[item_idx].identifier;
}

static bool brush_type_matches_active_tool(bContext *C, const int brush_type)
{
  const bToolRef *active_tool = toolsystem_active_tool_from_context_or_view3d(C);

  if (active_tool->runtime == nullptr) {
    /* Should only ever be null in background mode. */
    BLI_assert(G.background);
    return false;
  }

  if (!(active_tool->runtime->flag & TOOLREF_FLAG_USE_BRUSHES)) {
    return false;
  }

  BLI_assert(BKE_paintmode_get_active_from_context(C) == BKE_paintmode_get_from_tool(active_tool));
  return active_tool->runtime->brush_type == brush_type;
}

static NamedBrushAssetReference *toolsystem_brush_type_binding_lookup(const Paint *paint,
                                                                      const char *brush_type_name)
{
  return static_cast<NamedBrushAssetReference *>(
      BLI_findstring_ptr(&paint->tool_brush_bindings.active_brush_per_brush_type,
                         brush_type_name,
                         offsetof(NamedBrushAssetReference, name)));
}

/**
 * Update the bindings so the main brush reference matches the currently active brush.
 */
static void toolsystem_main_brush_binding_update_from_active(Paint *paint)
{
  MEM_delete(paint->tool_brush_bindings.main_brush_asset_reference);
  paint->tool_brush_bindings.main_brush_asset_reference = nullptr;

  if (paint->brush != nullptr) {
    if (std::optional<AssetWeakReference> brush_asset_reference =
            blender::bke::asset_edit_weak_reference_from_id(paint->brush->id))
    {
      paint->tool_brush_bindings.main_brush_asset_reference = MEM_new<AssetWeakReference>(
          __func__, *brush_asset_reference);
    }
  }
}

static void toolsystem_brush_type_binding_update(Paint *paint,
                                                 const PaintMode paint_mode,
                                                 const int brush_type)
{
  if (paint->brush == nullptr) {
    return;
  }
  const char *brush_type_name = brush_type_identifier_get(brush_type, paint_mode);
  if (!brush_type_name || !brush_type_name[0]) {
    return;
  }

  /* Update existing reference. */
  if (NamedBrushAssetReference *existing_brush_ref = toolsystem_brush_type_binding_lookup(
          paint, brush_type_name))
  {
    MEM_delete(existing_brush_ref->brush_asset_reference);
    existing_brush_ref->brush_asset_reference = MEM_new<AssetWeakReference>(
        __func__, *paint->brush_asset_reference);
  }
  /* Add new reference. */
  else {
    NamedBrushAssetReference *new_brush_ref = MEM_callocN<NamedBrushAssetReference>(__func__);

    new_brush_ref->name = BLI_strdup(brush_type_name);
    new_brush_ref->brush_asset_reference = MEM_new<AssetWeakReference>(
        __func__, *paint->brush_asset_reference);
    BLI_addhead(&paint->tool_brush_bindings.active_brush_per_brush_type, new_brush_ref);
  }
}

bool WM_toolsystem_activate_brush_and_tool(bContext *C, Paint *paint, Brush *brush)
{
  const bToolRef *active_tool = toolsystem_active_tool_from_context_or_view3d(C);
  const PaintMode paint_mode = BKE_paintmode_get_active_from_context(C);

  if (!BKE_paint_brush_poll(paint, brush)) {
    /* Avoid switching tool when brush isn't valid for this mode anyway. */
    return false;
  }

  /* If necessary, find a compatible tool to switch to. */
  {
    std::optional<int> brush_type = BKE_paint_get_brush_type_from_paintmode(brush, paint_mode);
    if (!brush_type) {
      BLI_assert_unreachable();
      WM_toolsystem_ref_set_by_id(C, "builtin.brush");
    }
    else if (!brush_type_matches_active_tool(C, *brush_type)) {
      const char *brush_type_name = brush_type_identifier_get(*brush_type, paint_mode);
      /* Calls into .py to query available tools. */
      toolsystem_ref_set_by_brush_type(C, brush_type_name);
    }
  }

  /* Do after switching tool, since switching tool will attempt to restore the last used brush of
   * that tool (in #toolsystem_brush_activate_from_toolref_for_object_paint()). */
  if (!BKE_paint_brush_set(paint, brush)) {
    return false;
  }

  if (active_tool->runtime->brush_type == -1) {
    /* Only update the main brush binding to reference the newly active brush. */
    toolsystem_main_brush_binding_update_from_active(paint);
  }
  else {
    toolsystem_brush_type_binding_update(paint, paint_mode, active_tool->runtime->brush_type);
  }

  return true;
}

static void toolsystem_brush_activate_from_toolref_for_object_particle(const Main *bmain,
                                                                       const WorkSpace *workspace,
                                                                       const bToolRef *tref)
{
  const bToolRef_Runtime *tref_rt = tref->runtime;

  if (!tref_rt->data_block[0]) {
    return;
  }

  const EnumPropertyItem *items = rna_enum_particle_edit_hair_brush_items;
  const int i = RNA_enum_from_identifier(items, tref_rt->data_block);
  if (i == -1) {
    return;
  }

  const wmWindowManager *wm = static_cast<wmWindowManager *>(bmain->wm.first);
  LISTBASE_FOREACH (wmWindow *, win, &wm->windows) {
    if (workspace == WM_window_get_active_workspace(win)) {
      Scene *scene = WM_window_get_active_scene(win);
      ToolSettings *ts = scene->toolsettings;
      ts->particle.brushtype = items[i].value;
    }
  }
}

static void toolsystem_brush_activate_from_toolref_for_object_paint(Main *bmain,
                                                                    const WorkSpace *workspace,
                                                                    const bToolRef *tref)
{
  bToolRef_Runtime *tref_rt = tref->runtime;

  const PaintMode paint_mode = BKE_paintmode_get_from_tool(tref);
  BLI_assert(paint_mode != PaintMode::Invalid);

  wmWindowManager *wm = static_cast<wmWindowManager *>(bmain->wm.first);
  LISTBASE_FOREACH (wmWindow *, win, &wm->windows) {
    if (workspace != WM_window_get_active_workspace(win)) {
      continue;
    }
    Scene *scene = WM_window_get_active_scene(win);
    BKE_paint_ensure_from_paintmode(scene, paint_mode);
    Paint *paint = BKE_paint_get_active_from_paintmode(scene, paint_mode);

    /* Attempt to re-activate a brush remembered for this brush type, as stored in a brush
     * binding. */
    if (tref_rt->brush_type != -1) {
      std::optional<AssetWeakReference> brush_asset_reference =
          [&]() -> std::optional<AssetWeakReference> {
        const char *brush_type_name = brush_type_identifier_get(tref_rt->brush_type, paint_mode);
        const NamedBrushAssetReference *brush_ref = toolsystem_brush_type_binding_lookup(
            paint, brush_type_name);

        if (brush_ref && brush_ref->brush_asset_reference) {
          return *brush_ref->brush_asset_reference;
        }
        /* No remembered brush found for this type, use a default for the type. */
        return BKE_paint_brush_type_default_reference(eObjectMode(paint->runtime.ob_mode),
                                                      tref_rt->brush_type);
      }();

      if (brush_asset_reference) {
        BKE_paint_brush_set(bmain, paint, &*brush_asset_reference);
      }
    }
    /* Re-activate the main brush, regardless of the brush type. */
    else {
      if (paint->tool_brush_bindings.main_brush_asset_reference) {
        BKE_paint_brush_set(bmain, paint, paint->tool_brush_bindings.main_brush_asset_reference);
        toolsystem_main_brush_binding_update_from_active(paint);
      }
      else {
        std::optional<AssetWeakReference> main_brush_asset_reference =
            [&]() -> std::optional<AssetWeakReference> {
          if (paint->tool_brush_bindings.main_brush_asset_reference) {
            return *paint->tool_brush_bindings.main_brush_asset_reference;
          }
          return BKE_paint_brush_type_default_reference(eObjectMode(paint->runtime.ob_mode),
                                                        std::nullopt);
        }();

        if (main_brush_asset_reference) {
          BKE_paint_brush_set(bmain, paint, &*main_brush_asset_reference);
          toolsystem_main_brush_binding_update_from_active(paint);
        }
      }
    }
  }
}

/**
 * Activate a brush compatible with \a tref, call when the active tool changes.
 */
static void toolsystem_brush_activate_from_toolref(Main *bmain,
                                                   const WorkSpace *workspace,
                                                   const bToolRef *tref)
{
  BLI_assert(tref->runtime->flag & TOOLREF_FLAG_USE_BRUSHES);

  if (tref->space_type == SPACE_VIEW3D) {
    if (tref->mode == CTX_MODE_PARTICLE) {
      toolsystem_brush_activate_from_toolref_for_object_particle(bmain, workspace, tref);
    }
    else {
      toolsystem_brush_activate_from_toolref_for_object_paint(bmain, workspace, tref);
    }
  }
  else if (tref->space_type == SPACE_IMAGE) {
    if (tref->mode == SI_MODE_PAINT) {
      toolsystem_brush_activate_from_toolref_for_object_paint(bmain, workspace, tref);
    }
  }
}

/**
 * Special case, the active brush data-block for the image & 3D viewport are shared.
 * This means changing the active brush tool in one space must change the tool
 * for the other space as well, see: #131062.
 */
static void toolsystem_brush_sync_for_texture_paint(Main *bmain,
                                                    WorkSpace *workspace,
                                                    bToolRef *tref)
{
  if (tref->space_type == SPACE_VIEW3D) {
    if (tref->mode == CTX_MODE_PAINT_TEXTURE) {
      bToolKey tkey{};
      tkey.space_type = SPACE_IMAGE;
      tkey.mode = SI_MODE_PAINT;
      bToolRef *tref_other = WM_toolsystem_ref_find(workspace, &tkey);
      if (tref_other) {
        toolsystem_ref_set_by_id_pending(bmain, tref_other, tref->idname);
      }
    }
  }
  else if (tref->space_type == SPACE_IMAGE) {
    if (tref->mode == SI_MODE_PAINT) {
      bToolKey tkey{};
      tkey.space_type = SPACE_VIEW3D;
      tkey.mode = CTX_MODE_PAINT_TEXTURE;
      bToolRef *tref_other = WM_toolsystem_ref_find(workspace, &tkey);
      if (tref_other) {
        toolsystem_ref_set_by_id_pending(bmain, tref_other, tref->idname);
      }
    }
  }
}
static void toolsystem_brush_clear_paint_reference(Main *bmain,
                                                   WorkSpace *workspace,
                                                   bToolRef *tref)
{
  const PaintMode paint_mode = BKE_paintmode_get_from_tool(tref);

  wmWindowManager *wm = static_cast<wmWindowManager *>(bmain->wm.first);
  LISTBASE_FOREACH (wmWindow *, win, &wm->windows) {
    if (workspace != WM_window_get_active_workspace(win)) {
      continue;
    }
    Scene *scene = WM_window_get_active_scene(win);
    if (Paint *paint = BKE_paint_get_active_from_paintmode(scene, paint_mode)) {
      BKE_paint_previous_asset_reference_clear(paint);
    }
  }
}

/** \} */

static void toolsystem_ref_link(Main *bmain, WorkSpace *workspace, bToolRef *tref)
{
  bToolRef_Runtime *tref_rt = tref->runtime;
  if (tref_rt->gizmo_group[0]) {
    const char *idname = tref_rt->gizmo_group;
    wmGizmoGroupType *gzgt = WM_gizmogrouptype_find(idname, false);
    if (gzgt != nullptr) {
      if ((gzgt->flag & WM_GIZMOGROUPTYPE_TOOL_INIT) == 0) {
        if (!WM_gizmo_group_type_ensure_ptr(gzgt)) {
          /* Even if the group-type was has been linked, it's possible the space types
           * were not previously using it. (happens with multiple windows). */
          wmGizmoMapType *gzmap_type = WM_gizmomaptype_ensure(&gzgt->gzmap_params);
          WM_gizmoconfig_update_tag_group_type_init(gzmap_type, gzgt);
        }
      }
    }
    else {
      CLOG_WARN(WM_LOG_TOOLS, "'%s' widget not found", idname);
    }
  }

  if (tref_rt->flag & TOOLREF_FLAG_USE_BRUSHES) {
    toolsystem_brush_activate_from_toolref(bmain, workspace, tref);
    toolsystem_brush_sync_for_texture_paint(bmain, workspace, tref);
  }
  else {
    toolsystem_brush_clear_paint_reference(bmain, workspace, tref);
  }
}

static void toolsystem_refresh_ref(const bContext *C, WorkSpace *workspace, bToolRef *tref)
{
  if (tref->runtime == nullptr) {
    return;
  }
  /* Currently same operation. */
  toolsystem_ref_link(CTX_data_main(C), workspace, tref);
}
void WM_toolsystem_refresh(const bContext *C, WorkSpace *workspace, const bToolKey *tkey)
{
  bToolRef *tref = WM_toolsystem_ref_find(workspace, tkey);
  if (tref) {
    toolsystem_refresh_ref(C, workspace, tref);
  }
}

static void toolsystem_reinit_ref(bContext *C, WorkSpace *workspace, bToolRef *tref)
{
  toolsystem_reinit_with_toolref(C, workspace, tref);
}
void WM_toolsystem_reinit(bContext *C, WorkSpace *workspace, const bToolKey *tkey)
{
  bToolRef *tref = WM_toolsystem_ref_find(workspace, tkey);
  if (tref) {
    toolsystem_reinit_ref(C, workspace, tref);
  }
}

void WM_toolsystem_unlink_all(bContext *C, WorkSpace *workspace)
{
  LISTBASE_FOREACH (bToolRef *, tref, &workspace->tools) {
    tref->tag = 0;
  }

  LISTBASE_FOREACH (bToolRef *, tref, &workspace->tools) {
    if (tref->runtime) {
      if (tref->tag == 0) {
        toolsystem_unlink_ref(C, workspace, tref);
        tref->tag = 1;
      }
    }
  }
}

void WM_toolsystem_refresh_all(const bContext *C, WorkSpace *workspace)
{
  BLI_assert(0);
  LISTBASE_FOREACH (bToolRef *, tref, &workspace->tools) {
    toolsystem_refresh_ref(C, workspace, tref);
  }
}
void WM_toolsystem_reinit_all(bContext *C, wmWindow *win)
{
  bScreen *screen = WM_window_get_active_screen(win);
  const Scene *scene = WM_window_get_active_scene(win);
  ViewLayer *view_layer = WM_window_get_active_view_layer(win);
  LISTBASE_FOREACH (ScrArea *, area, &screen->areabase) {
    if (((1 << area->spacetype) & WM_TOOLSYSTEM_SPACE_MASK) == 0) {
      continue;
    }

    WorkSpace *workspace = WM_window_get_active_workspace(win);
    bToolKey tkey{};
    tkey.space_type = area->spacetype;
    tkey.mode = WM_toolsystem_mode_from_spacetype(scene, view_layer, area, area->spacetype);
    bToolRef *tref = WM_toolsystem_ref_find(workspace, &tkey);
    if (tref) {
      if (tref->tag == 0) {
        toolsystem_reinit_ref(C, workspace, tref);
        tref->tag = 1;
      }
    }
  }
}

void WM_toolsystem_ref_set_from_runtime(bContext *C,
                                        WorkSpace *workspace,
                                        bToolRef *tref,
                                        const bToolRef_Runtime *tref_rt,
                                        const char *idname)
{
  Main *bmain = CTX_data_main(C);

  if (tref->runtime) {
    toolsystem_unlink_ref(C, workspace, tref);
  }

  STRNCPY(tref->idname, idname);

  /* This immediate request supersedes any unhandled pending requests. */
  tref->idname_pending[0] = '\0';

  if (tref->runtime == nullptr) {
    tref->runtime = MEM_callocN<bToolRef_Runtime>(__func__);
  }

  if (tref_rt != tref->runtime) {
    *tref->runtime = *tref_rt;
  }

  /* Ideally Python could check this gizmo group flag and not
   * pass in the argument to begin with. */
  bool use_fallback_keymap = false;

  if (tref->idname_fallback[0] || tref->runtime->keymap_fallback[0]) {
    if (tref_rt->flag & TOOLREF_FLAG_FALLBACK_KEYMAP) {
      use_fallback_keymap = true;
    }
    else if (tref_rt->gizmo_group[0]) {
      wmGizmoGroupType *gzgt = WM_gizmogrouptype_find(tref_rt->gizmo_group, false);
      if (gzgt) {
        if (gzgt->flag & WM_GIZMOGROUPTYPE_TOOL_FALLBACK_KEYMAP) {
          use_fallback_keymap = true;
        }
      }
    }
  }
  if (use_fallback_keymap == false) {
    tref->idname_fallback[0] = '\0';
    tref->runtime->keymap_fallback[0] = '\0';
  }

  toolsystem_ref_link(bmain, workspace, tref);

  toolsystem_refresh_screen_from_active_tool(bmain, workspace, tref);

  /* Set the cursor if possible, if not - it's fine as entering the region will refresh it. */
  {
    wmWindow *win = CTX_wm_window(C);
    if (win != nullptr) {
      win->addmousemove = true;
      win->tag_cursor_refresh = true;
    }
  }

  {
    wmMsgBus *mbus = CTX_wm_message_bus(C);
    WM_msg_publish_rna_prop(mbus, &workspace->id, workspace, WorkSpace, tools);
  }
}

void WM_toolsystem_ref_sync_from_context(Main *bmain, WorkSpace *workspace, bToolRef *tref)
{
  bToolRef_Runtime *tref_rt = tref->runtime;
  if ((tref_rt == nullptr) || (tref_rt->data_block[0] == '\0')) {
    return;
  }
  wmWindowManager *wm = static_cast<wmWindowManager *>(bmain->wm.first);
  LISTBASE_FOREACH (wmWindow *, win, &wm->windows) {
    if (workspace != WM_window_get_active_workspace(win)) {
      continue;
    }

    Scene *scene = WM_window_get_active_scene(win);
    ToolSettings *ts = scene->toolsettings;
    ViewLayer *view_layer = WM_window_get_active_view_layer(win);
    BKE_view_layer_synced_ensure(scene, view_layer);
    const Object *ob = BKE_view_layer_active_object_get(view_layer);
    if (ob == nullptr) {
      /* Pass. */
    }
    if ((tref->space_type == SPACE_VIEW3D) && (tref->mode == CTX_MODE_PARTICLE)) {
      if (ob->mode & OB_MODE_PARTICLE_EDIT) {
        const EnumPropertyItem *items = rna_enum_particle_edit_hair_brush_items;
        const int i = RNA_enum_from_value(items, ts->particle.brushtype);
        const EnumPropertyItem *item = &items[i];
        if (!STREQ(tref_rt->data_block, item->identifier)) {
          STRNCPY(tref_rt->data_block, item->identifier);
          SNPRINTF(tref->idname, "builtin_brush.%s", item->name);
        }
      }
    }
  }
}

void WM_toolsystem_init(const bContext *C)
{
  Main *bmain = CTX_data_main(C);

  BLI_assert(CTX_wm_window(C) == nullptr);

  LISTBASE_FOREACH (WorkSpace *, workspace, &bmain->workspaces) {
    LISTBASE_FOREACH (bToolRef *, tref, &workspace->tools) {
      MEM_SAFE_FREE(tref->runtime);
    }
  }

  /* Rely on screen initialization for gizmos. */
}

static bool toolsystem_key_ensure_check(const bToolKey *tkey)
{
  switch (tkey->space_type) {
    case SPACE_VIEW3D:
      return true;
    case SPACE_IMAGE:
      if (ELEM(tkey->mode, SI_MODE_PAINT, SI_MODE_UV, SI_MODE_VIEW)) {
        return true;
      }
      break;
    case SPACE_NODE:
      return true;
    case SPACE_SEQ:
      return true;
  }
  return false;
}

int WM_toolsystem_mode_from_spacetype(const Scene *scene,
                                      ViewLayer *view_layer,
                                      ScrArea *area,
                                      int space_type)
{
  int mode = -1;
  switch (space_type) {
    case SPACE_VIEW3D: {
      /* 'area' may be nullptr in this case. */
      BKE_view_layer_synced_ensure(scene, view_layer);
      Object *obact = BKE_view_layer_active_object_get(view_layer);
      if (obact != nullptr) {
        Object *obedit = OBEDIT_FROM_OBACT(obact);
        mode = CTX_data_mode_enum_ex(obedit, obact, eObjectMode(obact->mode));
      }
      else {
        mode = CTX_MODE_OBJECT;
      }
      break;
    }
    case SPACE_IMAGE: {
      SpaceImage *sima = static_cast<SpaceImage *>(area->spacedata.first);
      mode = sima->mode;
      break;
    }
    case SPACE_NODE: {
      mode = 0;
      break;
    }
    case SPACE_SEQ: {
      SpaceSeq *sseq = static_cast<SpaceSeq *>(area->spacedata.first);
      mode = sseq->view;
      break;
    }
  }
  return mode;
}

bool WM_toolsystem_key_from_context(const Scene *scene,
                                    ViewLayer *view_layer,
                                    ScrArea *area,
                                    bToolKey *tkey)
{
  int space_type = SPACE_EMPTY;
  int mode = -1;

  if (area != nullptr) {
    space_type = area->spacetype;
    mode = WM_toolsystem_mode_from_spacetype(scene, view_layer, area, space_type);
  }

  if (mode != -1) {
    tkey->space_type = space_type;
    tkey->mode = mode;
    return true;
  }
  return false;
}

void WM_toolsystem_refresh_active(bContext *C)
{
  Main *bmain = CTX_data_main(C);

  struct {
    wmWindow *win;
    ScrArea *area;
    ARegion *region;
    bool is_set;
  } context_prev = {nullptr};

  for (wmWindowManager *wm = static_cast<wmWindowManager *>(bmain->wm.first); wm;
       wm = static_cast<wmWindowManager *>(wm->id.next))
  {
    LISTBASE_FOREACH (wmWindow *, win, &wm->windows) {
      WorkSpace *workspace = WM_window_get_active_workspace(win);
      bScreen *screen = WM_window_get_active_screen(win);
      const Scene *scene = WM_window_get_active_scene(win);
      ViewLayer *view_layer = WM_window_get_active_view_layer(win);
      /* Could skip loop for modes that don't depend on space type. */
      int space_type_mask_handled = 0;
      LISTBASE_FOREACH (ScrArea *, area, &screen->areabase) {
        /* Don't change the space type of the active tool, only update its mode. */
        const int space_type_mask = (1 << area->spacetype);
        if ((space_type_mask & WM_TOOLSYSTEM_SPACE_MASK) &&
            ((space_type_mask_handled & space_type_mask) == 0))
        {
          space_type_mask_handled |= space_type_mask;
          bToolKey tkey{};
          tkey.space_type = area->spacetype;
          tkey.mode = WM_toolsystem_mode_from_spacetype(scene, view_layer, area, area->spacetype);
          bToolRef *tref = WM_toolsystem_ref_find(workspace, &tkey);
          if (tref != area->runtime.tool) {
            if (context_prev.is_set == false) {
              context_prev.win = CTX_wm_window(C);
              context_prev.area = CTX_wm_area(C);
              context_prev.region = CTX_wm_region(C);
              context_prev.is_set = true;
            }

            CTX_wm_window_set(C, win);
            CTX_wm_area_set(C, area);

            toolsystem_reinit_ensure_toolref(C, workspace, &tkey, nullptr);
          }
        }
      }
    }
  }

  if (context_prev.is_set) {
    CTX_wm_window_set(C, context_prev.win);
    CTX_wm_area_set(C, context_prev.area);
    CTX_wm_region_set(C, context_prev.region);
  }

  BKE_workspace_id_tag_all_visible(bmain, ID_TAG_DOIT);

  LISTBASE_FOREACH (WorkSpace *, workspace, &bmain->workspaces) {
    if (workspace->id.tag & ID_TAG_DOIT) {
      workspace->id.tag &= ~ID_TAG_DOIT;
      /* Refresh to ensure data is initialized.
       * This is needed because undo can load a state which no longer has the underlying DNA data
       * needed for the tool (un-initialized paint-slots for eg), see: #64339. */
      LISTBASE_FOREACH (bToolRef *, tref, &workspace->tools) {
        toolsystem_refresh_ref(C, workspace, tref);
      }
    }
  }
}

bool WM_toolsystem_refresh_screen_area(WorkSpace *workspace,
                                       const Scene *scene,
                                       ViewLayer *view_layer,
                                       ScrArea *area)
{
  const bool is_tool_set_prev = area->runtime.is_tool_set;
  const bToolRef *tref_prev = area->runtime.tool;

  area->runtime.tool = nullptr;
  area->runtime.is_tool_set = true;
  const int mode = WM_toolsystem_mode_from_spacetype(scene, view_layer, area, area->spacetype);
  LISTBASE_FOREACH (bToolRef *, tref, &workspace->tools) {
    if (tref->space_type == area->spacetype) {
      if (tref->mode == mode) {
        area->runtime.tool = tref;
        break;
      }
    }
  }
  return !(is_tool_set_prev && (tref_prev == area->runtime.tool));
}

void WM_toolsystem_refresh_screen_window(wmWindow *win)
{
  WorkSpace *workspace = WM_window_get_active_workspace(win);
  bool space_type_has_tools[SPACE_TYPE_NUM] = {false};
  LISTBASE_FOREACH (bToolRef *, tref, &workspace->tools) {
    space_type_has_tools[tref->space_type] = true;
  }
  bScreen *screen = WM_window_get_active_screen(win);
  const Scene *scene = WM_window_get_active_scene(win);
  ViewLayer *view_layer = WM_window_get_active_view_layer(win);
  LISTBASE_FOREACH (ScrArea *, area, &screen->areabase) {
    area->runtime.tool = nullptr;
    area->runtime.is_tool_set = true;
    if (space_type_has_tools[area->spacetype]) {
      WM_toolsystem_refresh_screen_area(workspace, scene, view_layer, area);
    }
  }
}

void WM_toolsystem_refresh_screen_all(Main *bmain)
{
  /* Update all ScrArea's tools. */
  for (wmWindowManager *wm = static_cast<wmWindowManager *>(bmain->wm.first); wm;
       wm = static_cast<wmWindowManager *>(wm->id.next))
  {
    LISTBASE_FOREACH (wmWindow *, win, &wm->windows) {
      WM_toolsystem_refresh_screen_window(win);
    }
  }
}

static void toolsystem_refresh_screen_from_active_tool(Main *bmain,
                                                       WorkSpace *workspace,
                                                       bToolRef *tref)
{
  /* Update all ScrArea's tools. */
  for (wmWindowManager *wm = static_cast<wmWindowManager *>(bmain->wm.first); wm;
       wm = static_cast<wmWindowManager *>(wm->id.next))
  {
    LISTBASE_FOREACH (wmWindow *, win, &wm->windows) {
      if (workspace == WM_window_get_active_workspace(win)) {
        bScreen *screen = WM_window_get_active_screen(win);
        const Scene *scene = WM_window_get_active_scene(win);
        ViewLayer *view_layer = WM_window_get_active_view_layer(win);
        LISTBASE_FOREACH (ScrArea *, area, &screen->areabase) {
          if (area->spacetype == tref->space_type) {
            int mode = WM_toolsystem_mode_from_spacetype(scene, view_layer, area, area->spacetype);
            if (mode == tref->mode) {
              area->runtime.tool = tref;
              area->runtime.is_tool_set = true;
            }
          }
        }
      }
    }
  }
}

bToolRef *WM_toolsystem_ref_set_by_id_ex(
    bContext *C, WorkSpace *workspace, const bToolKey *tkey, const char *name, bool cycle)
{
  wmOperatorType *ot = WM_operatortype_find("WM_OT_tool_set_by_id", false);
  /* On startup, Python operators are not yet loaded. */
  if (ot == nullptr) {
    return nullptr;
  }

/* Some contexts use the current space type (e.g. image editor),
 * ensure this is set correctly or there is no area. */
#ifndef NDEBUG
  /* Exclude this check for some space types where the space type isn't used. */
  if ((1 << tkey->space_type) & WM_TOOLSYSTEM_SPACE_MASK_MODE_FROM_SPACE) {
    ScrArea *area = CTX_wm_area(C);
    BLI_assert(area == nullptr || area->spacetype == tkey->space_type);
  }
#endif

  PointerRNA op_props;
  WM_operator_properties_create_ptr(&op_props, ot);
  RNA_string_set(&op_props, "name", name);

  BLI_assert((1 << tkey->space_type) & WM_TOOLSYSTEM_SPACE_MASK);

  RNA_enum_set(&op_props, "space_type", tkey->space_type);
  RNA_boolean_set(&op_props, "cycle", cycle);

  WM_operator_name_call_ptr(C, ot, WM_OP_EXEC_DEFAULT, &op_props, nullptr);
  WM_operator_properties_free(&op_props);

  bToolRef *tref = WM_toolsystem_ref_find(workspace, tkey);

  if (tref) {
    Main *bmain = CTX_data_main(C);
    toolsystem_refresh_screen_from_active_tool(bmain, workspace, tref);
  }

  return (tref && STREQ(tref->idname, name)) ? tref : nullptr;
}

bToolRef *WM_toolsystem_ref_set_by_id(bContext *C, const char *name)
{
  const Scene *scene = CTX_data_scene(C);
  ViewLayer *view_layer = CTX_data_view_layer(C);
  ScrArea *area = CTX_wm_area(C);
  bToolKey tkey;
  if (WM_toolsystem_key_from_context(scene, view_layer, area, &tkey)) {
    WorkSpace *workspace = CTX_wm_workspace(C);
    return WM_toolsystem_ref_set_by_id_ex(C, workspace, &tkey, name, false);
  }
  return nullptr;
}

static void toolsystem_ref_set_by_brush_type(bContext *C, const char *brush_type)
{
  const Scene *scene = CTX_data_scene(C);
  ViewLayer *view_layer = CTX_data_view_layer(C);
  ScrArea *area = CTX_wm_area(C);
  const bToolKey tkey = toolsystem_key_from_context_or_view3d(scene, view_layer, area);
  WorkSpace *workspace = CTX_wm_workspace(C);

  wmOperatorType *ot = WM_operatortype_find("WM_OT_tool_set_by_brush_type", false);
  /* On startup, Python operators are not yet loaded. */
  if (ot == nullptr) {
    return;
  }

/* Some contexts use the current space type (e.g. image editor),
 * ensure this is set correctly or there is no area. */
#ifndef NDEBUG
  /* Exclude this check for some space types where the space type isn't used. */
  if ((1 << tkey.space_type) & WM_TOOLSYSTEM_SPACE_MASK_MODE_FROM_SPACE) {
    ScrArea *area = CTX_wm_area(C);
    BLI_assert(area == nullptr || area->spacetype == tkey.space_type);
  }
#endif

  PointerRNA op_props;
  WM_operator_properties_create_ptr(&op_props, ot);
  RNA_string_set(&op_props, "brush_type", brush_type);

  BLI_assert((1 << tkey.space_type) & WM_TOOLSYSTEM_SPACE_MASK);

  RNA_enum_set(&op_props, "space_type", tkey.space_type);

  WM_operator_name_call_ptr(C, ot, WM_OP_EXEC_DEFAULT, &op_props, nullptr);
  WM_operator_properties_free(&op_props);

  bToolRef *tref = WM_toolsystem_ref_find(workspace, &tkey);

  if (tref) {
    Main *bmain = CTX_data_main(C);
    toolsystem_refresh_screen_from_active_tool(bmain, workspace, tref);
  }
}

/**
 * Request a tool ID be activated in a context where it's not known if the tool exists,
 * when the areas using this tool are not visible.
 * In this case, set the `idname` as pending and flag tools area for updating.
 *
 * If the tool doesn't exist then the current tool is to be left as-is.
 */
static void toolsystem_ref_set_by_id_pending(Main *bmain,
                                             bToolRef *tref,
                                             const char *idname_pending)
{
  BLI_assert(idname_pending[0]);

  /* Check if the pending or current tool is already set to the requested value. */
  const bool this_match = STREQ(idname_pending, tref->idname);
  if (tref->idname_pending[0]) {
    const bool next_match = STREQ(idname_pending, tref->idname_pending);
    if (next_match) {
      return;
    }
    /* Highly unlikely but possible the current active tool matches the name.
     * In this case clear pending as there is nothing to do. */
    if (this_match) {
      tref->idname_pending[0] = '\0';
      return;
    }
  }
  else {
    if (this_match) {
      return;
    }
  }

  STRNCPY(tref->idname_pending, idname_pending);

  /* If there would be a convenient way to know which screens used which work-spaces,
   * that could be used here. */
  LISTBASE_FOREACH (bScreen *, screen, &bmain->screens) {
    LISTBASE_FOREACH (ScrArea *, area, &screen->areabase) {
      if (area->runtime.tool == tref) {
        area->runtime.tool = nullptr;
        area->runtime.is_tool_set = false;
        area->flag |= AREA_FLAG_ACTIVE_TOOL_UPDATE;
      }
    }
  }
}

static void toolsystem_reinit_with_toolref(bContext *C, WorkSpace *workspace, bToolRef *tref)
{
  bToolKey tkey{};
  tkey.space_type = tref->space_type;
  tkey.mode = tref->mode;

  const char *idname = tref->idname_pending[0] ? tref->idname_pending : tref->idname;

  WM_toolsystem_ref_set_by_id_ex(C, workspace, &tkey, idname, false);

  /* Never attempt the pending name again, if it's not found, no need to keep trying. */
  tref->idname_pending[0] = '\0';
}

static const char *toolsystem_default_tool(const bToolKey *tkey)
{
  switch (tkey->space_type) {
    case SPACE_VIEW3D:
      switch (tkey->mode) {
        case CTX_MODE_SCULPT:
        case CTX_MODE_PAINT_VERTEX:
        case CTX_MODE_PAINT_WEIGHT:
        case CTX_MODE_PAINT_TEXTURE:
        case CTX_MODE_PAINT_GPENCIL_LEGACY:
        case CTX_MODE_PAINT_GREASE_PENCIL:
        case CTX_MODE_SCULPT_GPENCIL_LEGACY:
        case CTX_MODE_SCULPT_GREASE_PENCIL:
        case CTX_MODE_WEIGHT_GPENCIL_LEGACY:
        case CTX_MODE_WEIGHT_GREASE_PENCIL:
        case CTX_MODE_VERTEX_GPENCIL_LEGACY:
        case CTX_MODE_VERTEX_GREASE_PENCIL:
        case CTX_MODE_SCULPT_CURVES:
          return "builtin.brush";
        case CTX_MODE_PARTICLE:
          return "builtin_brush.Comb";
        case CTX_MODE_EDIT_TEXT:
          return "builtin.select_text";
      }
      break;
    case SPACE_IMAGE:
      switch (tkey->mode) {
        case SI_MODE_PAINT:
          return "builtin.brush";
        case SI_MODE_VIEW:
          return "builtin.sample";
      }
      break;
    case SPACE_NODE: {
      return "builtin.select_box";
    }
    case SPACE_SEQ: {
      return "builtin.select_box";
    }
  }

  return "builtin.select_box";
}

/**
 * Run after changing modes.
 */
static bToolRef *toolsystem_reinit_ensure_toolref(bContext *C,
                                                  WorkSpace *workspace,
                                                  const bToolKey *tkey,
                                                  const char *default_tool)
{
  bToolRef *tref;
  if (WM_toolsystem_ref_ensure(workspace, tkey, &tref)) {
    if (default_tool == nullptr) {
      default_tool = toolsystem_default_tool(tkey);
    }
    STRNCPY(tref->idname, default_tool);
  }
  toolsystem_reinit_with_toolref(C, workspace, tref);
  return tref;
}

static void wm_toolsystem_update_from_context_view3d_impl(bContext *C, WorkSpace *workspace)
{
  const Scene *scene = CTX_data_scene(C);
  ViewLayer *view_layer = CTX_data_view_layer(C);
  int space_type = SPACE_VIEW3D;
  bToolKey tkey{};
  tkey.space_type = space_type;
  tkey.mode = WM_toolsystem_mode_from_spacetype(scene, view_layer, nullptr, space_type);
  toolsystem_reinit_ensure_toolref(C, workspace, &tkey, nullptr);
}

void WM_toolsystem_update_from_context_view3d(bContext *C)
{
  WorkSpace *workspace = CTX_wm_workspace(C);
  if (workspace) {
    wm_toolsystem_update_from_context_view3d_impl(C, workspace);
  }

  /* Multi window support. */
  Main *bmain = CTX_data_main(C);
  wmWindowManager *wm = static_cast<wmWindowManager *>(bmain->wm.first);
  if (!BLI_listbase_is_single(&wm->windows)) {
    wmWindow *win_prev = CTX_wm_window(C);
    ScrArea *area_prev = CTX_wm_area(C);
    ARegion *region_prev = CTX_wm_region(C);

    LISTBASE_FOREACH (wmWindow *, win, &wm->windows) {
      if (win != win_prev) {
        WorkSpace *workspace_iter = WM_window_get_active_workspace(win);
        if (workspace_iter != workspace) {

          CTX_wm_window_set(C, win);

          wm_toolsystem_update_from_context_view3d_impl(C, workspace_iter);

          CTX_wm_window_set(C, win_prev);
          CTX_wm_area_set(C, area_prev);
          CTX_wm_region_set(C, region_prev);
        }
      }
    }
  }
}

void WM_toolsystem_update_from_context(
    bContext *C, WorkSpace *workspace, const Scene *scene, ViewLayer *view_layer, ScrArea *area)
{
  bToolKey tkey{};
  tkey.space_type = area->spacetype;
  tkey.mode = WM_toolsystem_mode_from_spacetype(scene, view_layer, area, area->spacetype);
  if (toolsystem_key_ensure_check(&tkey)) {
    toolsystem_reinit_ensure_toolref(C, workspace, &tkey, nullptr);
  }
}

bool WM_toolsystem_active_tool_is_brush(const bContext *C)
{
  const bToolRef_Runtime *tref_rt = WM_toolsystem_runtime_from_context((bContext *)C);
  return tref_rt && (tref_rt->flag & TOOLREF_FLAG_USE_BRUSHES);
}

bool WM_toolsystem_active_tool_has_custom_cursor(const bContext *C)
{
  const bToolRef_Runtime *tref_rt = WM_toolsystem_runtime_from_context((bContext *)C);
  return tref_rt && (tref_rt->cursor != WM_CURSOR_DEFAULT);
}

void WM_toolsystem_do_msg_notify_tag_refresh(bContext *C,
                                             wmMsgSubscribeKey * /*msg_key*/,
                                             wmMsgSubscribeValue *msg_val)
{
  ScrArea *area = static_cast<ScrArea *>(msg_val->user_data);
  Main *bmain = CTX_data_main(C);
  wmWindow *win = static_cast<wmWindow *>(((wmWindowManager *)bmain->wm.first)->windows.first);
  if (win->next != nullptr) {
    do {
      bScreen *screen = WM_window_get_active_screen(win);
      if (BLI_findindex(&screen->areabase, area) != -1) {
        break;
      }
    } while ((win = win->next));
  }

  WorkSpace *workspace = WM_window_get_active_workspace(win);
  const Scene *scene = WM_window_get_active_scene(win);
  ViewLayer *view_layer = WM_window_get_active_view_layer(win);

  bToolKey tkey{};
  tkey.space_type = area->spacetype;
  tkey.mode = WM_toolsystem_mode_from_spacetype(scene, view_layer, area, area->spacetype);
  WM_toolsystem_refresh(C, workspace, &tkey);
  WM_toolsystem_refresh_screen_area(workspace, scene, view_layer, area);
}

static IDProperty *idprops_ensure_named_group(IDProperty *group, const char *idname)
{
  IDProperty *prop = IDP_GetPropertyFromGroup(group, idname);
  if ((prop == nullptr) || (prop->type != IDP_GROUP)) {
    prop = blender::bke::idprop::create_group(__func__).release();
    STRNCPY(prop->name, idname);
    IDP_ReplaceInGroup_ex(group, prop, nullptr, 0);
  }
  return prop;
}

IDProperty *WM_toolsystem_ref_properties_get_idprops(bToolRef *tref)
{
  IDProperty *group = tref->properties;
  if (group == nullptr) {
    return nullptr;
  }
  return IDP_GetPropertyFromGroup(group, tref->idname);
}

IDProperty *WM_toolsystem_ref_properties_ensure_idprops(bToolRef *tref)
{
  if (tref->properties == nullptr) {
    tref->properties = blender::bke::idprop::create_group(__func__).release();
  }
  return idprops_ensure_named_group(tref->properties, tref->idname);
}

bool WM_toolsystem_ref_properties_get_ex(bToolRef *tref,
                                         const char *idname,
                                         StructRNA *type,
                                         PointerRNA *r_ptr)
{
  IDProperty *group = WM_toolsystem_ref_properties_get_idprops(tref);
  IDProperty *prop = group ? IDP_GetPropertyFromGroup(group, idname) : nullptr;
  *r_ptr = RNA_pointer_create_discrete(nullptr, type, prop);
  return (prop != nullptr);
}

void WM_toolsystem_ref_properties_ensure_ex(bToolRef *tref,
                                            const char *idname,
                                            StructRNA *type,
                                            PointerRNA *r_ptr)
{
  IDProperty *group = WM_toolsystem_ref_properties_ensure_idprops(tref);
  IDProperty *prop = idprops_ensure_named_group(group, idname);
  *r_ptr = RNA_pointer_create_discrete(nullptr, type, prop);
}

void WM_toolsystem_ref_properties_init_for_keymap(bToolRef *tref,
                                                  PointerRNA *dst_ptr,
                                                  PointerRNA *src_ptr,
                                                  wmOperatorType *ot)
{
  *dst_ptr = *src_ptr;
  if (dst_ptr->data) {
    dst_ptr->data = IDP_CopyProperty(static_cast<const IDProperty *>(dst_ptr->data));
  }
  else {
    dst_ptr->data = blender::bke::idprop::create_group("wmOpItemProp").release();
  }
  IDProperty *group = WM_toolsystem_ref_properties_get_idprops(tref);
  if (group != nullptr) {
    IDProperty *prop = IDP_GetPropertyFromGroup(group, ot->idname);
    if (prop) {
      /* Important key-map items properties don't get overwritten by the tools.
       * - When a key-map item doesn't set a property, the tool-systems is used.
       * - When it does, it overrides the tool-system.
       *
       * This way the default action can be to follow the top-bar tool-settings &
       * modifier keys can be used to perform different actions that aren't clobbered here.
       */
      IDP_MergeGroup(static_cast<IDProperty *>(dst_ptr->data), prop, false);
    }
  }
}
