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

#include <fmt/format.h>
#include <sstream>

#include "BKE_compute_contexts.hh"
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_modifier.hh"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "BKE_screen.hh"

#include "BLI_string.h"

#include "BLT_translation.hh"

#include "DNA_modifier_types.h"

#include "ED_object.hh"
#include "ED_screen.hh"
#include "ED_undo.hh"

#include "MOD_nodes.hh"
#include "NOD_geometry.hh"
#include "NOD_geometry_nodes_caller_ui.hh"
#include "NOD_geometry_nodes_log.hh"
#include "NOD_socket_usage_inference.hh"

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

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

#include "intern/MOD_ui_common.hh"

namespace blender::nodes {

namespace geo_log = geo_eval_log;

namespace {
struct PanelOpenProperty {
  PointerRNA ptr;
  StringRefNull name;
};

struct SearchInfo {
  geo_log::GeoTreeLog *tree_log = nullptr;
  bNodeTree *tree = nullptr;
  IDProperty *properties = nullptr;
};

struct ModifierSearchData {
  uint32_t object_session_uid;
  char modifier_name[MAX_NAME];
};

struct OperatorSearchData {
  /** Can store this data directly, because it's more persistent than for the modifier. */
  SearchInfo info;
};

struct SocketSearchData {
  std::variant<ModifierSearchData, OperatorSearchData> search_data;
  char socket_identifier[MAX_NAME];
  bool is_output;

  SearchInfo info(const bContext &C) const;
};
/* This class must not have a destructor, since it is used by buttons and freed with #MEM_freeN. */
BLI_STATIC_ASSERT(std::is_trivially_destructible_v<SocketSearchData>, "");

struct DrawGroupInputsContext {
  const bContext &C;
  bNodeTree *tree;
  geo_log::GeoTreeLog *tree_log;
  nodes::PropertiesVectorSet properties;
  PointerRNA *properties_ptr;
  PointerRNA *bmain_ptr;
  Array<nodes::socket_usage_inference::SocketUsage> input_usages;
  bool use_name_for_ids = false;
  std::function<PanelOpenProperty(const bNodeTreeInterfacePanel &)> panel_open_property_fn;
  std::function<SocketSearchData(const bNodeTreeInterfaceSocket &)> socket_search_data_fn;
  std::function<void(uiLayout &, int icon, const bNodeTreeInterfaceSocket &)>
      draw_attribute_toggle_fn;

  bool input_is_visible(const bNodeTreeInterfaceSocket &socket) const
  {
    return this->input_usages[this->tree->interface_input_index(socket)].is_visible;
  }

  bool input_is_active(const bNodeTreeInterfaceSocket &socket) const
  {
    return this->input_usages[this->tree->interface_input_index(socket)].is_used;
  }
};
}  // namespace

static geo_log::GeoTreeLog *get_root_tree_log(const NodesModifierData &nmd)
{
  if (!nmd.runtime->eval_log) {
    return nullptr;
  }
  bke::ModifierComputeContext compute_context{nullptr, nmd};
  return &nmd.runtime->eval_log->get_tree_log(compute_context.hash());
}

static NodesModifierData *get_modifier_data(Main &bmain,
                                            const wmWindowManager &wm,
                                            const ModifierSearchData &data)
{
  if (ED_screen_animation_playing(&wm)) {
    /* Work around an issue where the attribute search exec function has stale pointers when data
     * is reallocated when evaluating the node tree, causing a crash. This would be solved by
     * allowing the UI search data to own arbitrary memory rather than just referencing it. */
    return nullptr;
  }

  const Object *object = (Object *)BKE_libblock_find_session_uid(
      &bmain, ID_OB, data.object_session_uid);
  if (object == nullptr) {
    return nullptr;
  }
  ModifierData *md = BKE_modifiers_findby_name(object, data.modifier_name);
  if (md == nullptr) {
    return nullptr;
  }
  BLI_assert(md->type == eModifierType_Nodes);
  return reinterpret_cast<NodesModifierData *>(md);
}

SearchInfo SocketSearchData::info(const bContext &C) const
{
  if (const auto *modifier_search_data = std::get_if<ModifierSearchData>(&this->search_data)) {
    const NodesModifierData *nmd = get_modifier_data(
        *CTX_data_main(&C), *CTX_wm_manager(&C), *modifier_search_data);
    if (nmd == nullptr) {
      return {};
    }
    if (nmd->node_group == nullptr) {
      return {};
    }
    geo_log::GeoTreeLog *tree_log = get_root_tree_log(*nmd);
    return {tree_log, nmd->node_group, nmd->settings.properties};
  }
  if (const auto *operator_search_data = std::get_if<OperatorSearchData>(&this->search_data)) {
    return operator_search_data->info;
  }
  return {};
}

static void layer_name_search_update_fn(
    const bContext *C, void *arg, const char *str, uiSearchItems *items, const bool is_first)
{
  const SocketSearchData &data = *static_cast<SocketSearchData *>(arg);
  const SearchInfo info = data.info(*C);
  if (!info.tree || !info.tree_log) {
    return;
  }
  info.tree_log->ensure_layer_names();
  info.tree->ensure_topology_cache();

  Vector<const bNodeSocket *> sockets_to_check;
  for (const bNode *node : info.tree->group_input_nodes()) {
    for (const bNodeSocket *socket : node->output_sockets()) {
      if (socket->type == SOCK_GEOMETRY) {
        sockets_to_check.append(socket);
      }
    }
  }

  Set<StringRef> names;
  Vector<const std::string *> layer_names;
  for (const bNodeSocket *socket : sockets_to_check) {
    const geo_log::ValueLog *value_log = info.tree_log->find_socket_value_log(*socket);
    if (value_log == nullptr) {
      continue;
    }
    if (const auto *geo_log = dynamic_cast<const geo_log::GeometryInfoLog *>(value_log)) {
      if (const std::optional<geo_log::GeometryInfoLog::GreasePencilInfo> &grease_pencil_info =
              geo_log->grease_pencil_info)
      {
        for (const std::string &name : grease_pencil_info->layer_names) {
          if (names.add(name)) {
            layer_names.append(&name);
          }
        }
      }
    }
  }
  BLI_assert(items);
  ui::grease_pencil_layer_search_add_items(str, layer_names.as_span(), *items, is_first);
}

static void layer_name_search_exec_fn(bContext *C, void *data_v, void *item_v)
{
  const SocketSearchData &data = *static_cast<SocketSearchData *>(data_v);
  const std::string *item = static_cast<std::string *>(item_v);
  if (!item) {
    return;
  }
  const SearchInfo info = data.info(*C);
  if (!info.properties) {
    return;
  }

  IDProperty &name_property = *IDP_GetPropertyFromGroup(info.properties, data.socket_identifier);
  IDP_AssignString(&name_property, item->c_str());

  ED_undo_push(C, "Assign Layer Name");
}

static void add_layer_name_search_button(DrawGroupInputsContext &ctx,
                                         uiLayout *layout,
                                         const bNodeTreeInterfaceSocket &socket)
{
  const std::string rna_path = fmt::format("[\"{}\"]", BLI_str_escape(socket.identifier));
  if (!ctx.tree_log) {
    layout->prop(ctx.properties_ptr, rna_path, UI_ITEM_NONE, "", ICON_NONE);
    return;
  }

  uiLayoutSetPropDecorate(layout, false);

  uiLayout *split = &layout->split(0.4f, false);
  uiLayout *name_row = &split->row(false);
  uiLayoutSetAlignment(name_row, UI_LAYOUT_ALIGN_RIGHT);

  name_row->label(socket.name ? IFACE_(socket.name) : "", ICON_NONE);
  uiLayout *prop_row = &split->row(true);

  uiBlock *block = uiLayoutGetBlock(prop_row);
  uiBut *but = uiDefIconTextButR(block,
                                 UI_BTYPE_SEARCH_MENU,
                                 0,
                                 ICON_OUTLINER_DATA_GP_LAYER,
                                 "",
                                 0,
                                 0,
                                 10 * UI_UNIT_X, /* Dummy value, replaced by layout system. */
                                 UI_UNIT_Y,
                                 ctx.properties_ptr,
                                 rna_path,
                                 0,
                                 0.0f,
                                 0.0f,
                                 StringRef(socket.description));
  UI_but_placeholder_set(but, "Layer");
  layout->label("", ICON_BLANK1);

  const Object *object = ed::object::context_object(&ctx.C);
  BLI_assert(object != nullptr);
  if (object == nullptr) {
    return;
  }

  /* Using a custom free function make the search not work currently. So make sure this data can be
   * freed with MEM_freeN. */
  SocketSearchData *data = static_cast<SocketSearchData *>(
      MEM_mallocN(sizeof(SocketSearchData), __func__));
  *data = ctx.socket_search_data_fn(socket);
  UI_but_func_search_set_results_are_suggestions(but, true);
  UI_but_func_search_set_sep_string(but, UI_MENU_ARROW_SEP);
  UI_but_func_search_set(but,
                         nullptr,
                         layer_name_search_update_fn,
                         data,
                         true,
                         nullptr,
                         layer_name_search_exec_fn,
                         nullptr);
}

static void attribute_search_update_fn(
    const bContext *C, void *arg, const char *str, uiSearchItems *items, const bool is_first)
{
  SocketSearchData &data = *static_cast<SocketSearchData *>(arg);
  const SearchInfo info = data.info(*C);
  if (!info.tree || !info.tree_log) {
    return;
  }
  info.tree_log->ensure_existing_attributes();
  info.tree->ensure_topology_cache();

  Vector<const bNodeSocket *> sockets_to_check;
  if (data.is_output) {
    for (const bNode *node : info.tree->nodes_by_type("NodeGroupOutput")) {
      for (const bNodeSocket *socket : node->input_sockets()) {
        if (socket->type == SOCK_GEOMETRY) {
          sockets_to_check.append(socket);
        }
      }
    }
  }
  else {
    for (const bNode *node : info.tree->group_input_nodes()) {
      for (const bNodeSocket *socket : node->output_sockets()) {
        if (socket->type == SOCK_GEOMETRY) {
          sockets_to_check.append(socket);
        }
      }
    }
  }
  Set<StringRef> names;
  Vector<const geo_log::GeometryAttributeInfo *> attributes;
  for (const bNodeSocket *socket : sockets_to_check) {
    const geo_log::ValueLog *value_log = info.tree_log->find_socket_value_log(*socket);
    if (value_log == nullptr) {
      continue;
    }
    if (const auto *geo_log = dynamic_cast<const geo_log::GeometryInfoLog *>(value_log)) {
      for (const geo_log::GeometryAttributeInfo &attribute : geo_log->attributes) {
        if (names.add(attribute.name)) {
          attributes.append(&attribute);
        }
      }
    }
  }
  ui::attribute_search_add_items(str, data.is_output, attributes.as_span(), items, is_first);
}

static void attribute_search_exec_fn(bContext *C, void *data_v, void *item_v)
{
  if (item_v == nullptr) {
    return;
  }
  SocketSearchData &data = *static_cast<SocketSearchData *>(data_v);
  const auto &item = *static_cast<const geo_log::GeometryAttributeInfo *>(item_v);
  const SearchInfo info = data.info(*C);
  if (!info.properties) {
    return;
  }

  const std::string attribute_prop_name = data.socket_identifier +
                                          nodes::input_attribute_name_suffix;
  IDProperty &name_property = *IDP_GetPropertyFromGroup(info.properties, attribute_prop_name);
  IDP_AssignString(&name_property, item.name.c_str());

  ED_undo_push(C, "Assign Attribute Name");
}

static void add_attribute_search_button(DrawGroupInputsContext &ctx,
                                        uiLayout *layout,
                                        const StringRefNull rna_path_attribute_name,
                                        const bNodeTreeInterfaceSocket &socket)
{
  if (!ctx.tree_log) {
    layout->prop(ctx.properties_ptr, rna_path_attribute_name, UI_ITEM_NONE, "", ICON_NONE);
    return;
  }

  uiBlock *block = uiLayoutGetBlock(layout);
  uiBut *but = uiDefIconTextButR(block,
                                 UI_BTYPE_SEARCH_MENU,
                                 0,
                                 ICON_NONE,
                                 "",
                                 0,
                                 0,
                                 10 * UI_UNIT_X, /* Dummy value, replaced by layout system. */
                                 UI_UNIT_Y,
                                 ctx.properties_ptr,
                                 rna_path_attribute_name,
                                 0,
                                 0.0f,
                                 0.0f,
                                 StringRef(socket.description));

  const Object *object = ed::object::context_object(&ctx.C);
  BLI_assert(object != nullptr);
  if (object == nullptr) {
    return;
  }

  /* Using a custom free function make the search not work currently. So make sure this data can be
   * freed with MEM_freeN. */
  SocketSearchData *data = static_cast<SocketSearchData *>(
      MEM_mallocN(sizeof(SocketSearchData), __func__));
  *data = ctx.socket_search_data_fn(socket);
  UI_but_func_search_set_results_are_suggestions(but, true);
  UI_but_func_search_set_sep_string(but, UI_MENU_ARROW_SEP);
  UI_but_func_search_set(but,
                         nullptr,
                         attribute_search_update_fn,
                         data,
                         true,
                         nullptr,
                         attribute_search_exec_fn,
                         nullptr);

  char *attribute_name = RNA_string_get_alloc(
      ctx.properties_ptr, rna_path_attribute_name.c_str(), nullptr, 0, nullptr);
  const bool access_allowed = bke::allow_procedural_attribute_access(attribute_name);
  MEM_freeN(attribute_name);
  if (!access_allowed) {
    UI_but_flag_enable(but, UI_BUT_REDALERT);
  }
}

static void add_attribute_search_or_value_buttons(
    DrawGroupInputsContext &ctx,
    uiLayout *layout,
    const StringRefNull rna_path,
    const bNodeTreeInterfaceSocket &socket,
    const std::optional<StringRefNull> use_name = std::nullopt)
{
  const bke::bNodeSocketType *typeinfo = socket.socket_typeinfo();
  const eNodeSocketDatatype type = typeinfo ? typeinfo->type : SOCK_CUSTOM;
  const std::string rna_path_attribute_name = fmt::format(
      "[\"{}{}\"]", BLI_str_escape(socket.identifier), nodes::input_attribute_name_suffix);

  /* We're handling this manually in this case. */
  uiLayoutSetPropDecorate(layout, false);

  uiLayout *split = &layout->split(0.4f, false);
  uiLayout *name_row = &split->row(false);
  uiLayoutSetAlignment(name_row, UI_LAYOUT_ALIGN_RIGHT);

  uiLayout *prop_row = nullptr;

  const std::optional<StringRef> attribute_name = nodes::input_attribute_name_get(ctx.properties,
                                                                                  socket);
  const StringRefNull socket_name = use_name.has_value() ?
                                        (*use_name) :
                                        (socket.name ? IFACE_(socket.name) : "");
  if (type == SOCK_BOOLEAN && !attribute_name) {
    name_row->label("", ICON_NONE);
    prop_row = &split->row(true);
  }
  else {
    prop_row = &layout->row(true);
  }

  if (type == SOCK_BOOLEAN) {
    uiLayoutSetPropSep(prop_row, false);
    uiLayoutSetAlignment(prop_row, UI_LAYOUT_ALIGN_EXPAND);
  }

  if (attribute_name) {
    name_row->label(IFACE_(socket_name), ICON_NONE);
    prop_row = &split->row(true);
    add_attribute_search_button(ctx, prop_row, rna_path_attribute_name, socket);
    layout->label("", ICON_BLANK1);
  }
  else {
    const char *name = IFACE_(socket_name.c_str());
    prop_row->prop(ctx.properties_ptr, rna_path, UI_ITEM_NONE, name, ICON_NONE);
    uiItemDecoratorR(layout, ctx.properties_ptr, rna_path.c_str(), -1);
  }

  ctx.draw_attribute_toggle_fn(*prop_row, ICON_SPREADSHEET, socket);
}

static NodesModifierPanel *find_panel_by_id(NodesModifierData &nmd, const int id)
{
  for (const int i : IndexRange(nmd.panels_num)) {
    if (nmd.panels[i].id == id) {
      return &nmd.panels[i];
    }
  }
  return nullptr;
}

/* Drawing the properties manually with #uiLayout::prop instead of #uiDefAutoButsRNA allows using
 * the node socket identifier for the property names, since they are unique, but also having
 * the correct label displayed in the UI. */
static void draw_property_for_socket(DrawGroupInputsContext &ctx,
                                     uiLayout *layout,
                                     const bNodeTreeInterfaceSocket &socket,
                                     const std::optional<StringRefNull> parent_name = std::nullopt)
{
  const StringRefNull identifier = socket.identifier;
  /* The property should be created in #MOD_nodes_update_interface with the correct type. */
  IDProperty *property = ctx.properties.lookup_key_default_as(identifier, nullptr);

  /* IDProperties can be removed with python, so there could be a situation where
   * there isn't a property for a socket or it doesn't have the correct type. */
  if (property == nullptr ||
      !nodes::id_property_type_matches_socket(socket, *property, ctx.use_name_for_ids))
  {
    return;
  }

  const int input_index = ctx.tree->interface_input_index(socket);
  if (!ctx.input_is_visible(socket)) {
    /* The input is not used currently, but it would be used if any menu input is changed.
     * By convention, the input is hidden in this case instead of just grayed out. */
    return;
  }

  uiLayout *row = &layout->row(true);
  uiLayoutSetPropDecorate(row, true);
  uiLayoutSetActive(row, ctx.input_is_active(socket));

  const std::string rna_path = fmt::format("[\"{}\"]", BLI_str_escape(identifier.c_str()));

  /* Use #uiItemPointerR to draw pointer properties because #uiLayout::prop would not have enough
   * information about what type of ID to select for editing the values. This is because
   * pointer IDProperties contain no information about their type. */
  const bke::bNodeSocketType *typeinfo = socket.socket_typeinfo();
  const eNodeSocketDatatype type = typeinfo ? typeinfo->type : SOCK_CUSTOM;
  std::string name = socket.name ? IFACE_(socket.name) : "";

  /* If the property has a prefix that's the same string as the name of the panel it's in, remove
   * the prefix so it appears less verbose. */
  if (parent_name.has_value()) {
    const StringRefNull prefix_to_remove = *parent_name;
    int pos = name.find(prefix_to_remove);
    if (pos == 0 && name != prefix_to_remove) {
      /* Needs to trim remainig space characters if any. Use the `trim()` from `StringRefNull`
       * because std::string doesn't have a built-in `trim()` yet. If the property name is the
       * same as parent panel's name then keep the name, otherwise the name would be an empty
       * string which messes up the UI. */
      name = StringRefNull(name.substr(prefix_to_remove.size())).trim();
    }
  }

  switch (type) {
    case SOCK_OBJECT: {
      uiItemPointerR(
          row, ctx.properties_ptr, rna_path, ctx.bmain_ptr, "objects", name, ICON_OBJECT_DATA);
      break;
    }
    case SOCK_COLLECTION: {
      uiItemPointerR(row,
                     ctx.properties_ptr,
                     rna_path,
                     ctx.bmain_ptr,
                     "collections",
                     name,
                     ICON_OUTLINER_COLLECTION);
      break;
    }
    case SOCK_MATERIAL: {
      uiItemPointerR(
          row, ctx.properties_ptr, rna_path, ctx.bmain_ptr, "materials", name, ICON_MATERIAL);
      break;
    }
    case SOCK_TEXTURE: {
      uiItemPointerR(
          row, ctx.properties_ptr, rna_path, ctx.bmain_ptr, "textures", name, ICON_TEXTURE);
      break;
    }
    case SOCK_IMAGE: {
      uiTemplateID(row,
                   &ctx.C,
                   ctx.properties_ptr,
                   rna_path,
                   "image.new",
                   "image.open",
                   nullptr,
                   UI_TEMPLATE_ID_FILTER_ALL,
                   false,
                   name);
      break;
    }
    case SOCK_MENU: {
      if (socket.flag & NODE_INTERFACE_SOCKET_MENU_EXPANDED) {
        /* Use a single space when the name is empty to work around a bug with expanded enums. Also
         * see #ui_item_enum_expand_exec. */
        row->prop(ctx.properties_ptr,
                  rna_path,
                  UI_ITEM_R_EXPAND,
                  StringRef(name).is_empty() ? " " : name,
                  ICON_NONE);
      }
      else {
        row->prop(ctx.properties_ptr, rna_path, UI_ITEM_NONE, name, ICON_NONE);
      }
      break;
    }
    case SOCK_BOOLEAN: {
      if (is_layer_selection_field(socket)) {
        add_layer_name_search_button(ctx, row, socket);
        /* Adds a spacing at the end of the row. */
        row->label("", ICON_BLANK1);
        break;
      }
      ATTR_FALLTHROUGH;
    }
    default: {
      if (nodes::input_has_attribute_toggle(*ctx.tree, input_index)) {
        add_attribute_search_or_value_buttons(ctx, row, rna_path, socket, name);
      }
      else {
        row->prop(ctx.properties_ptr, rna_path, UI_ITEM_NONE, name, ICON_NONE);
      }
    }
  }
  if (!nodes::input_has_attribute_toggle(*ctx.tree, input_index)) {
    row->label("", ICON_BLANK1);
  }
}

static bool interface_panel_has_socket(DrawGroupInputsContext &ctx,
                                       const bNodeTreeInterfacePanel &interface_panel)
{
  for (const bNodeTreeInterfaceItem *item : interface_panel.items()) {
    if (item->item_type == NODE_INTERFACE_SOCKET) {
      const bNodeTreeInterfaceSocket &socket = *reinterpret_cast<const bNodeTreeInterfaceSocket *>(
          item);
      if (socket.flag & NODE_INTERFACE_SOCKET_HIDE_IN_MODIFIER) {
        continue;
      }
      if (socket.flag & NODE_INTERFACE_SOCKET_INPUT) {
        if (ctx.input_is_visible(socket)) {
          return true;
        }
      }
    }
    else if (item->item_type == NODE_INTERFACE_PANEL) {
      if (interface_panel_has_socket(ctx,
                                     *reinterpret_cast<const bNodeTreeInterfacePanel *>(item)))
      {
        return true;
      }
    }
  }
  return false;
}

static bool interface_panel_affects_output(DrawGroupInputsContext &ctx,
                                           const bNodeTreeInterfacePanel &panel)
{
  for (const bNodeTreeInterfaceItem *item : panel.items()) {
    if (item->item_type == NODE_INTERFACE_SOCKET) {
      const auto &socket = *reinterpret_cast<const bNodeTreeInterfaceSocket *>(item);
      if (socket.flag & NODE_INTERFACE_SOCKET_HIDE_IN_MODIFIER) {
        continue;
      }
      if (!(socket.flag & NODE_INTERFACE_SOCKET_INPUT)) {
        continue;
      }
      if (ctx.input_is_active(socket)) {
        return true;
      }
    }
    else if (item->item_type == NODE_INTERFACE_PANEL) {
      const auto &sub_interface_panel = *reinterpret_cast<const bNodeTreeInterfacePanel *>(item);
      if (interface_panel_affects_output(ctx, sub_interface_panel)) {
        return true;
      }
    }
  }
  return false;
}

static void draw_interface_panel_content(
    DrawGroupInputsContext &ctx,
    uiLayout *layout,
    const bNodeTreeInterfacePanel &interface_panel,
    const bool skip_first = false,
    const std::optional<StringRefNull> parent_name = std::nullopt)
{
  for (const bNodeTreeInterfaceItem *item : interface_panel.items().drop_front(skip_first ? 1 : 0))
  {
    switch (NodeTreeInterfaceItemType(item->item_type)) {
      case NODE_INTERFACE_PANEL: {
        const auto &sub_interface_panel = *reinterpret_cast<const bNodeTreeInterfacePanel *>(item);
        if (!interface_panel_has_socket(ctx, sub_interface_panel)) {
          continue;
        }
        PanelOpenProperty open_property = ctx.panel_open_property_fn(sub_interface_panel);
        PanelLayout panel_layout;
        bool skip_first = false;
        /* Check if the panel should have a toggle in the header. */
        const bNodeTreeInterfaceSocket *toggle_socket = sub_interface_panel.header_toggle_socket();
        if (toggle_socket && !(toggle_socket->flag & NODE_INTERFACE_SOCKET_HIDE_IN_MODIFIER)) {
          const StringRefNull identifier = toggle_socket->identifier;
          IDProperty *property = ctx.properties.lookup_key_default_as(identifier, nullptr);
          /* IDProperties can be removed with python, so there could be a situation where
           * there isn't a property for a socket or it doesn't have the correct type. */
          if (property == nullptr || !nodes::id_property_type_matches_socket(
                                         *toggle_socket, *property, ctx.use_name_for_ids))
          {
            continue;
          }
          char socket_id_esc[MAX_NAME * 2];
          BLI_str_escape(socket_id_esc, identifier.c_str(), sizeof(socket_id_esc));

          char rna_path[sizeof(socket_id_esc) + 4];
          SNPRINTF(rna_path, "[\"%s\"]", socket_id_esc);

          panel_layout = layout->panel_prop_with_bool_header(&ctx.C,
                                                             &open_property.ptr,
                                                             open_property.name,
                                                             ctx.properties_ptr,
                                                             rna_path,
                                                             IFACE_(sub_interface_panel.name));
          skip_first = true;
        }
        else {
          panel_layout = layout->panel_prop(&ctx.C, &open_property.ptr, open_property.name);
          panel_layout.header->label(IFACE_(sub_interface_panel.name), ICON_NONE);
        }
        if (!interface_panel_affects_output(ctx, sub_interface_panel)) {
          uiLayoutSetActive(panel_layout.header, false);
        }
        uiLayoutSetTooltipFunc(
            panel_layout.header,
            [](bContext * /*C*/, void *panel_arg, const StringRef /*tip*/) -> std::string {
              const auto *panel = static_cast<bNodeTreeInterfacePanel *>(panel_arg);
              return StringRef(panel->description);
            },
            const_cast<bNodeTreeInterfacePanel *>(&sub_interface_panel),
            nullptr,
            nullptr);
        if (panel_layout.body) {
          const StringRefNull panel_name = sub_interface_panel.name ? sub_interface_panel.name :
                                                                      "";
          draw_interface_panel_content(
              ctx, panel_layout.body, sub_interface_panel, skip_first, panel_name);
        }
        break;
      }
      case NODE_INTERFACE_SOCKET: {
        const auto &interface_socket = *reinterpret_cast<const bNodeTreeInterfaceSocket *>(item);
        if (interface_socket.flag & NODE_INTERFACE_SOCKET_INPUT) {
          if (!(interface_socket.flag & NODE_INTERFACE_SOCKET_HIDE_IN_MODIFIER)) {
            draw_property_for_socket(ctx, layout, interface_socket, parent_name);
          }
        }
        break;
      }
    }
  }
}

static void draw_warnings(const bContext *C,
                          const NodesModifierData &nmd,
                          uiLayout *layout,
                          PointerRNA *md_ptr)
{
  if (G.is_rendering) {
    /* Avoid accessing this data while baking in a separate thread. */
    return;
  }
  using namespace geo_log;
  GeoTreeLog *tree_log = get_root_tree_log(nmd);
  if (!tree_log) {
    return;
  }
  tree_log->ensure_node_warnings(*CTX_data_main(C));
  const int warnings_num = tree_log->all_warnings.size();
  if (warnings_num == 0) {
    return;
  }
  PanelLayout panel = layout->panel_prop(C, md_ptr, "open_warnings_panel");
  panel.header->label(fmt::format(fmt::runtime(IFACE_("Warnings ({})")), warnings_num).c_str(),
                      ICON_NONE);
  if (!panel.body) {
    return;
  }
  Vector<const NodeWarning *> warnings(tree_log->all_warnings.size());
  for (const int i : warnings.index_range()) {
    warnings[i] = &tree_log->all_warnings[i];
  }
  std::sort(warnings.begin(), warnings.end(), [](const NodeWarning *a, const NodeWarning *b) {
    const int severity_a = node_warning_type_severity(a->type);
    const int severity_b = node_warning_type_severity(b->type);
    if (severity_a > severity_b) {
      return true;
    }
    if (severity_a < severity_b) {
      return false;
    }
    return BLI_strcasecmp_natural(a->message.c_str(), b->message.c_str()) < 0;
  });

  uiLayout *col = &panel.body->column(false);
  for (const NodeWarning *warning : warnings) {
    const int icon = node_warning_type_icon(warning->type);
    col->label(warning->message, icon);
  }
}

static bool has_output_attribute(const bNodeTree *tree)
{
  if (!tree) {
    return false;
  }
  for (const bNodeTreeInterfaceSocket *interface_socket : tree->interface_outputs()) {
    const bke::bNodeSocketType *typeinfo = interface_socket->socket_typeinfo();
    const eNodeSocketDatatype type = typeinfo ? typeinfo->type : SOCK_CUSTOM;
    if (nodes::socket_type_has_attribute_toggle(type)) {
      return true;
    }
  }
  return false;
}

static void draw_property_for_output_socket(DrawGroupInputsContext &ctx,
                                            uiLayout *layout,
                                            const bNodeTreeInterfaceSocket &socket)
{
  const std::string rna_path_attribute_name = fmt::format(
      "[\"{}{}\"]", BLI_str_escape(socket.identifier), nodes::input_attribute_name_suffix);

  uiLayout *split = &layout->split(0.4f, false);
  uiLayout *name_row = &split->row(false);
  uiLayoutSetAlignment(name_row, UI_LAYOUT_ALIGN_RIGHT);
  name_row->label(socket.name ? socket.name : "", ICON_NONE);

  uiLayout *row = &split->row(true);
  add_attribute_search_button(ctx, row, rna_path_attribute_name, socket);
}

static void draw_output_attributes_panel(DrawGroupInputsContext &ctx, uiLayout *layout)
{
  if (ctx.tree != nullptr && !ctx.properties.is_empty()) {
    for (const bNodeTreeInterfaceSocket *socket : ctx.tree->interface_outputs()) {
      const bke::bNodeSocketType *typeinfo = socket->socket_typeinfo();
      const eNodeSocketDatatype type = typeinfo ? typeinfo->type : SOCK_CUSTOM;
      if (nodes::socket_type_has_attribute_toggle(type)) {
        draw_property_for_output_socket(ctx, layout, *socket);
      }
    }
  }
}

static void draw_bake_panel(uiLayout *layout, PointerRNA *modifier_ptr)
{
  uiLayout *col = &layout->column(false);
  uiLayoutSetPropSep(col, true);
  uiLayoutSetPropDecorate(col, false);
  col->prop(modifier_ptr, "bake_target", UI_ITEM_NONE, std::nullopt, ICON_NONE);
  col->prop(modifier_ptr, "bake_directory", UI_ITEM_NONE, IFACE_("Bake Path"), ICON_NONE);
}

static void draw_named_attributes_panel(uiLayout *layout, NodesModifierData &nmd)
{
  if (G.is_rendering) {
    /* Avoid accessing this data while baking in a separate thread. */
    return;
  }
  geo_log::GeoTreeLog *tree_log = get_root_tree_log(nmd);
  if (tree_log == nullptr) {
    return;
  }

  tree_log->ensure_used_named_attributes();
  const Map<StringRefNull, geo_log::NamedAttributeUsage> &usage_by_attribute =
      tree_log->used_named_attributes;

  if (usage_by_attribute.is_empty()) {
    layout->label(RPT_("No named attributes used"), ICON_INFO);
    return;
  }

  struct NameWithUsage {
    StringRefNull name;
    geo_log::NamedAttributeUsage usage;
  };

  Vector<NameWithUsage> sorted_used_attribute;
  for (auto &&item : usage_by_attribute.items()) {
    sorted_used_attribute.append({item.key, item.value});
  }
  std::sort(sorted_used_attribute.begin(),
            sorted_used_attribute.end(),
            [](const NameWithUsage &a, const NameWithUsage &b) {
              return BLI_strcasecmp_natural(a.name.c_str(), b.name.c_str()) < 0;
            });

  for (const NameWithUsage &attribute : sorted_used_attribute) {
    const StringRef attribute_name = attribute.name;
    const geo_log::NamedAttributeUsage usage = attribute.usage;

    /* #uiLayoutRowWithHeading doesn't seem to work in this case. */
    uiLayout *split = &layout->split(0.4f, false);

    std::stringstream ss;
    Vector<std::string> usages;
    if ((usage & geo_log::NamedAttributeUsage::Read) != geo_log::NamedAttributeUsage::None) {
      usages.append(IFACE_("Read"));
    }
    if ((usage & geo_log::NamedAttributeUsage::Write) != geo_log::NamedAttributeUsage::None) {
      usages.append(IFACE_("Write"));
    }
    if ((usage & geo_log::NamedAttributeUsage::Remove) != geo_log::NamedAttributeUsage::None) {
      usages.append(CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, "Remove"));
    }
    for (const int i : usages.index_range()) {
      ss << usages[i];
      if (i < usages.size() - 1) {
        ss << ", ";
      }
    }

    uiLayout *row = &split->row(false);
    uiLayoutSetAlignment(row, UI_LAYOUT_ALIGN_RIGHT);
    uiLayoutSetActive(row, false);
    row->label(ss.str(), ICON_NONE);

    row = &split->row(false);
    row->label(attribute_name, ICON_NONE);
  }
}

static void draw_manage_panel(const bContext *C,
                              uiLayout *layout,
                              PointerRNA *modifier_ptr,
                              NodesModifierData &nmd)
{
  if (uiLayout *panel_layout = layout->panel_prop(
          C, modifier_ptr, "open_bake_panel", IFACE_("Bake")))
  {
    draw_bake_panel(panel_layout, modifier_ptr);
  }
  if (uiLayout *panel_layout = layout->panel_prop(
          C, modifier_ptr, "open_named_attributes_panel", IFACE_("Named Attributes")))
  {
    draw_named_attributes_panel(panel_layout, nmd);
  }
}

void draw_geometry_nodes_modifier_ui(const bContext &C, PointerRNA *modifier_ptr, uiLayout &layout)
{
  Main *bmain = CTX_data_main(&C);
  PointerRNA bmain_ptr = RNA_main_pointer_create(bmain);
  NodesModifierData &nmd = *modifier_ptr->data_as<NodesModifierData>();
  Object &object = *reinterpret_cast<Object *>(modifier_ptr->owner_id);

  DrawGroupInputsContext ctx{C,
                             nmd.node_group,
                             get_root_tree_log(nmd),
                             nodes::build_properties_vector_set(nmd.settings.properties),
                             modifier_ptr,
                             &bmain_ptr};

  ctx.panel_open_property_fn = [&](const bNodeTreeInterfacePanel &io_panel) -> PanelOpenProperty {
    NodesModifierPanel *panel = find_panel_by_id(nmd, io_panel.identifier);
    PointerRNA panel_ptr = RNA_pointer_create_discrete(
        modifier_ptr->owner_id, &RNA_NodesModifierPanel, panel);
    return {panel_ptr, "is_open"};
  };
  ctx.socket_search_data_fn = [&](const bNodeTreeInterfaceSocket &io_socket) -> SocketSearchData {
    SocketSearchData data{};
    ModifierSearchData &modifier_search_data = data.search_data.emplace<ModifierSearchData>();
    modifier_search_data.object_session_uid = object.id.session_uid;
    STRNCPY(modifier_search_data.modifier_name, nmd.modifier.name);
    STRNCPY(data.socket_identifier, io_socket.identifier);
    data.is_output = io_socket.flag & NODE_INTERFACE_SOCKET_OUTPUT;
    return data;
  };
  ctx.draw_attribute_toggle_fn =
      [&](uiLayout &layout, const int icon, const bNodeTreeInterfaceSocket &io_socket) {
        PointerRNA props = layout.op("object.geometry_nodes_input_attribute_toggle",
                                     "",
                                     icon,
                                     WM_OP_INVOKE_DEFAULT,
                                     UI_ITEM_NONE);
        RNA_string_set(&props, "modifier_name", nmd.modifier.name);
        RNA_string_set(&props, "input_name", io_socket.identifier);
      };

  uiLayoutSetPropSep(&layout, true);
  /* Decorators are added manually for supported properties because the
   * attribute/value toggle requires a manually built layout anyway. */
  uiLayoutSetPropDecorate(&layout, false);

  if (!(nmd.flag & NODES_MODIFIER_HIDE_DATABLOCK_SELECTOR)) {
    const char *newop = (nmd.node_group == nullptr) ? "node.new_geometry_node_group_assign" :
                                                      "object.geometry_node_tree_copy_assign";
    uiTemplateID(&layout, &C, modifier_ptr, "node_group", newop, nullptr, nullptr);
  }

  if (nmd.node_group != nullptr && nmd.settings.properties != nullptr) {
    nmd.node_group->ensure_interface_cache();
    ctx.input_usages.reinitialize(nmd.node_group->interface_inputs().size());
    nodes::socket_usage_inference::infer_group_interface_inputs_usage(
        *nmd.node_group, ctx.properties, ctx.input_usages);
    draw_interface_panel_content(ctx, &layout, nmd.node_group->tree_interface.root_panel);
  }

  modifier_error_message_draw(&layout, modifier_ptr);

  draw_warnings(&C, nmd, &layout, modifier_ptr);

  if (has_output_attribute(nmd.node_group)) {
    if (uiLayout *panel_layout = layout.panel_prop(
            &C, modifier_ptr, "open_output_attributes_panel", IFACE_("Output Attributes")))
    {
      draw_output_attributes_panel(ctx, panel_layout);
    }
  }

  if (uiLayout *panel_layout = layout.panel_prop(
          &C, modifier_ptr, "open_manage_panel", IFACE_("Manage")))
  {
    draw_manage_panel(&C, panel_layout, modifier_ptr, nmd);
  }
}

void draw_geometry_nodes_operator_redo_ui(const bContext &C,
                                          wmOperator &op,
                                          bNodeTree &tree,
                                          geo_eval_log::GeoTreeLog *tree_log)
{
  uiLayout &layout = *op.layout;
  Main &bmain = *CTX_data_main(&C);
  PointerRNA bmain_ptr = RNA_main_pointer_create(&bmain);

  DrawGroupInputsContext ctx{
      C, &tree, tree_log, nodes::build_properties_vector_set(op.properties), op.ptr, &bmain_ptr};
  ctx.panel_open_property_fn = [&](const bNodeTreeInterfacePanel &io_panel) -> PanelOpenProperty {
    Panel *root_panel = uiLayoutGetRootPanel(&layout);
    LayoutPanelState *state = BKE_panel_layout_panel_state_ensure(
        root_panel,
        "node_operator_panel_" + std::to_string(io_panel.identifier),
        io_panel.flag & NODE_INTERFACE_PANEL_DEFAULT_CLOSED);
    PointerRNA state_ptr = RNA_pointer_create_discrete(nullptr, &RNA_LayoutPanelState, state);
    return {state_ptr, "is_open"};
  };
  ctx.socket_search_data_fn = [&](const bNodeTreeInterfaceSocket &io_socket) -> SocketSearchData {
    SocketSearchData data{};
    OperatorSearchData &operator_search_data = data.search_data.emplace<OperatorSearchData>();
    operator_search_data.info.tree = &tree;
    operator_search_data.info.tree_log = tree_log;
    operator_search_data.info.properties = op.properties;
    STRNCPY(data.socket_identifier, io_socket.identifier);
    data.is_output = io_socket.flag & NODE_INTERFACE_SOCKET_OUTPUT;
    return data;
  };
  ctx.draw_attribute_toggle_fn =
      [&](uiLayout &layout, const int icon, const bNodeTreeInterfaceSocket &io_socket) {
        const std::string prop_name = fmt::format(
            "[\"{}{}\"]", BLI_str_escape(io_socket.identifier), nodes::input_use_attribute_suffix);
        layout.prop(op.ptr, prop_name, UI_ITEM_R_ICON_ONLY, "", icon);
      };
  ctx.use_name_for_ids = true;

  uiLayoutSetPropSep(&layout, true);
  /* Decorators are added manually for supported properties because the
   * attribute/value toggle requires a manually built layout anyway. */
  uiLayoutSetPropDecorate(&layout, false);

  tree.ensure_interface_cache();
  ctx.input_usages.reinitialize(tree.interface_inputs().size());
  nodes::socket_usage_inference::infer_group_interface_inputs_usage(
      tree, ctx.properties, ctx.input_usages);
  draw_interface_panel_content(ctx, &layout, tree.tree_interface.root_panel);
}

}  // namespace blender::nodes
