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

/** \file
 * \ingroup edinterface
 */

#include <cstdio>
#include <memory>

#include <fmt/format.h>

#include "BLI_assert.h"
#include "BLI_listbase.h"

#include "BLT_translation.hh"

#include "DNA_collection_types.h"
#include "DNA_object_types.h"

#include "BKE_collection.hh"
#include "BKE_context.hh"
#include "BKE_light_linking.h"
#include "BKE_scene.hh"

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

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

#include "WM_api.hh"

#include "ED_undo.hh"

namespace blender::ui {
namespace light_linking {

namespace {

class CollectionDropTarget {
  Collection &collection_;

 public:
  bool can_drop(const wmDrag &drag, const char **r_disabled_hint) const
  {
    if (drag.type != WM_DRAG_ID) {
      return false;
    }

    const wmDragID *drag_id = static_cast<wmDragID *>(drag.ids.first);
    if (!drag_id) {
      return false;
    }

    /* The dragged IDs are guaranteed to be the same type, so only check the type of the first one.
     */
    const ID_Type id_type = GS(drag_id->id->name);
    if (!ELEM(id_type, ID_OB, ID_GR)) {
      *r_disabled_hint = "Can only add objects and collections to the light linking collection";
      return false;
    }

    return true;
  }

  CollectionDropTarget(Collection &collection) : collection_(collection) {}

  Collection &get_collection() const
  {
    return collection_;
  }
};

class CollectionInsertDropTarget : public CollectionDropTarget {
  Main &bmain_;

 public:
  bool can_drop(const wmDrag &drag, const char **r_disabled_hint) const
  {
    if (!CollectionDropTarget::can_drop(drag, r_disabled_hint)) {
      return false;
    }

    bool have_ids_outside_collection = false;
    for (const wmDragID &drag_id : drag.ids) {
      const ID *id = drag_id.id;

      /* Allow insertion if the ID is already in the target collection.
       * This is to support reordering within the target collection. */
      const ID_Type id_type = GS(id->name);
      switch (id_type) {
        case ID_OB: {
          const Object *drag_object = id_cast<const Object *>(id);
          if (!BKE_collection_has_object(&get_collection(), drag_object)) {
            have_ids_outside_collection = true;
          }
          break;
        }
        case ID_GR: {
          const Collection *drag_collection = id_cast<const Collection *>(id);
          if (!BKE_collection_has_collection(&get_collection(), drag_collection)) {
            have_ids_outside_collection = true;
          }
          break;
        }
        default:
          break;
      }

      if (have_ids_outside_collection) {
        break;
      }
    }

    if (have_ids_outside_collection) {
      if (BKE_scene_find_from_collection(&bmain_, &get_collection()) != nullptr) {
        *r_disabled_hint = "Can not modify collection that is used by a scene";
        return false;
      }
    }

    return true;
  }

  CollectionInsertDropTarget(Main &bmain, Collection &collection)
      : CollectionDropTarget(collection), bmain_(bmain)
  {
  }
};

/**
 * Drop target for the view (when dropping into empty space of the view), not for an item.
 */
class InsertCollectionDropTarget : public DropTargetInterface {
  CollectionInsertDropTarget collection_target_;

 public:
  InsertCollectionDropTarget(Main &bmain, Collection &collection)
      : collection_target_(bmain, collection)
  {
  }

  bool can_drop(const wmDrag &drag, const char **r_disabled_hint) const override
  {
    return collection_target_.can_drop(drag, r_disabled_hint);
  }

  std::string drop_tooltip(const DragInfo & /*drag*/) const override
  {
    return TIP_("Add to linking collection");
  }

  bool on_drop(bContext *C, const DragInfo &drag) const override
  {
    Main *bmain = CTX_data_main(C);
    Scene *scene = CTX_data_scene(C);

    for (wmDragID &drag_id : drag.drag_data.ids) {
      BKE_light_linking_add_receiver_to_collection(bmain,
                                                   &collection_target_.get_collection(),
                                                   drag_id.id,
                                                   COLLECTION_LIGHT_LINKING_STATE_INCLUDE);
    }

    /* It is possible that the light linking collection is also used by the view layer.
     * For this case send a notifier so that the UI is updated for the changes in the collection
     * content. */
    WM_event_add_notifier(C, NC_SCENE | ND_LAYER_CONTENT, scene);

    ED_undo_push(C, "Add to linking collection");

    return true;
  }
};

class ReorderCollectionDropTarget : public TreeViewItemDropTarget {
  CollectionDropTarget collection_target_;
  const ID &drop_id_;

 public:
  ReorderCollectionDropTarget(AbstractTreeViewItem &item,
                              Collection &collection,
                              const ID &drop_id)
      : TreeViewItemDropTarget(item, DropBehavior::Reorder),
        collection_target_(collection),
        drop_id_(drop_id)
  {
  }

  bool can_drop(const wmDrag &drag, const char **r_disabled_hint) const override
  {
    return collection_target_.can_drop(drag, r_disabled_hint);
  }

  std::string drop_tooltip(const DragInfo &drag) const override
  {
    const StringRef drop_name = drop_id_.name + 2;

    switch (drag.drop_location) {
      case DropLocation::Into:
        return "Add to linking collection";
      case DropLocation::Before:
        return fmt::format(fmt::runtime(TIP_("Add to linking collection before {}")), drop_name);
      case DropLocation::After:
        return fmt::format(fmt::runtime(TIP_("Add to linking collection after {}")), drop_name);
    }

    return "";
  }

  bool on_drop(bContext *C, const DragInfo &drag) const override
  {
    Main *bmain = CTX_data_main(C);
    Scene *scene = CTX_data_scene(C);

    Collection &collection = collection_target_.get_collection();
    const eCollectionLightLinkingState link_state = COLLECTION_LIGHT_LINKING_STATE_INCLUDE;

    for (wmDragID &drag_id : drag.drag_data.ids) {
      if (drag_id.id == &drop_id_) {
        continue;
      }

      BKE_light_linking_unlink_id_from_collection(bmain, &collection, drag_id.id, nullptr);

      switch (drag.drop_location) {
        case DropLocation::Into:
          BKE_light_linking_add_receiver_to_collection(bmain, &collection, drag_id.id, link_state);
          break;
        case DropLocation::Before:
          BKE_light_linking_add_receiver_to_collection_before(
              bmain, &collection, drag_id.id, &drop_id_, link_state);
          break;
        case DropLocation::After:
          BKE_light_linking_add_receiver_to_collection_after(
              bmain, &collection, drag_id.id, &drop_id_, link_state);
          break;
      }
    }

    /* It is possible that the light linking collection is also used by the view layer.
     * For this case send a notifier so that the UI is updated for the changes in the collection
     * content. */
    WM_event_add_notifier(C, NC_SCENE | ND_LAYER_CONTENT, scene);

    ED_undo_push(C, "Add to linking collection");

    return true;
  }
};

class ItemDragController : public AbstractViewItemDragController {
  ID &id_;

 public:
  ItemDragController(AbstractView &view, ID &id) : AbstractViewItemDragController(view), id_(id) {}

  std::optional<eWM_DragDataType> get_drag_type() const override
  {
    return WM_DRAG_ID;
  }

  void *create_drag_data() const override
  {
    return static_cast<void *>(&id_);
  }
};

class CollectionViewItem : public BasicTreeViewItem {
  Layout &context_layout_;
  Collection &collection_;

  ID &id_;
  CollectionLightLinking &collection_light_linking_;

 public:
  CollectionViewItem(Layout &context_layout,
                     Collection &collection,
                     ID &id,
                     CollectionLightLinking &collection_light_linking,
                     const BIFIconID icon)
      : BasicTreeViewItem(id.name + 2, icon),
        context_layout_(context_layout),
        collection_(collection),
        id_(id),
        collection_light_linking_(collection_light_linking)
  {
  }

  void build_row(Layout &row) override
  {
    if (is_active()) {
      PointerRNA id_ptr = RNA_id_pointer_create(&id_);
      PointerRNA collection_ptr = RNA_id_pointer_create(&collection_.id);

      context_layout_.context_ptr_set("id", &id_ptr);
      context_layout_.context_ptr_set("collection", &collection_ptr);
    }

    add_label(row);

    Layout &sub = row.row(true);
    sub.use_property_decorate_set(false);

    build_state_button(sub);
  }

  std::unique_ptr<AbstractViewItemDragController> create_drag_controller() const override
  {
    return std::make_unique<ItemDragController>(get_tree_view(), id_);
  }

  std::unique_ptr<TreeViewItemDropTarget> create_drop_target() override
  {
    return std::make_unique<ReorderCollectionDropTarget>(*this, collection_, id_);
  }

 private:
  int get_state_icon() const
  {
    switch (collection_light_linking_.link_state) {
      case COLLECTION_LIGHT_LINKING_STATE_INCLUDE:
        return ICON_CHECKBOX_HLT;
      case COLLECTION_LIGHT_LINKING_STATE_EXCLUDE:
        return ICON_CHECKBOX_DEHLT;
    }
    BLI_assert_unreachable();
    return ICON_NONE;
  }

  static void link_state_toggle(CollectionLightLinking &collection_light_linking)
  {
    switch (collection_light_linking.link_state) {
      case COLLECTION_LIGHT_LINKING_STATE_INCLUDE:
        collection_light_linking.link_state = COLLECTION_LIGHT_LINKING_STATE_EXCLUDE;
        return;
      case COLLECTION_LIGHT_LINKING_STATE_EXCLUDE:
        collection_light_linking.link_state = COLLECTION_LIGHT_LINKING_STATE_INCLUDE;
        return;
    }

    BLI_assert_unreachable();
  }

  void build_state_button(Layout &row)
  {
    Block *block = row.block();
    const int icon = get_state_icon();

    PointerRNA collection_light_linking_ptr = RNA_pointer_create_discrete(
        &collection_.id, RNA_CollectionLightLinking, &collection_light_linking_);

    Button *button = uiDefIconButR(block,
                                   ButtonType::But,
                                   icon,
                                   0,
                                   0,
                                   UI_UNIT_X,
                                   UI_UNIT_Y,
                                   &collection_light_linking_ptr,
                                   "link_state",
                                   0,
                                   0.0f,
                                   0.0f,
                                   std::nullopt);

    button_func_set(button, [&collection_light_linking = collection_light_linking_](bContext &) {
      link_state_toggle(collection_light_linking);
    });
  }
};

class CollectionView : public AbstractTreeView {
  Layout &context_layout_;
  Main &bmain_;
  Collection &collection_;

 public:
  CollectionView(Layout &context_layout, Main &bmain, Collection &collection)
      : context_layout_(context_layout), bmain_(bmain), collection_(collection)
  {
  }

  void build_tree() override
  {
    for (CollectionChild &collection_child : collection_.children) {
      Collection *child_collection = collection_child.collection;
      add_tree_item<CollectionViewItem>(context_layout_,
                                        collection_,
                                        child_collection->id,
                                        collection_child.light_linking,
                                        ICON_OUTLINER_COLLECTION);
    }

    for (CollectionObject &collection_object : collection_.gobject) {
      Object *child_object = collection_object.ob;
      add_tree_item<CollectionViewItem>(context_layout_,
                                        collection_,
                                        child_object->id,
                                        collection_object.light_linking,
                                        ICON_OBJECT_DATA);
    }
  }

  std::unique_ptr<DropTargetInterface> create_drop_target() override
  {
    return std::make_unique<InsertCollectionDropTarget>(bmain_, collection_);
  }
};

}  // namespace
}  // namespace light_linking

void template_light_linking_collection(Layout *layout,
                                       bContext *C,
                                       Layout *context_layout,
                                       PointerRNA *ptr,
                                       const StringRefNull propname)
{
  if (!ptr->data) {
    return;
  }

  PropertyRNA *prop = RNA_struct_find_property(ptr, propname.c_str());
  if (!prop) {
    printf("%s: property not found: %s.%s\n",
           __func__,
           RNA_struct_identifier(ptr->type),
           propname.c_str());
    return;
  }

  if (RNA_property_type(prop) != PROP_POINTER) {
    printf("%s: expected pointer property for %s.%s\n",
           __func__,
           RNA_struct_identifier(ptr->type),
           propname.c_str());
    return;
  }

  const PointerRNA collection_ptr = RNA_property_pointer_get(ptr, prop);
  if (!collection_ptr.data) {
    return;
  }
  if (collection_ptr.type != RNA_Collection) {
    printf("%s: expected collection pointer property for %s.%s\n",
           __func__,
           RNA_struct_identifier(ptr->type),
           propname.c_str());
    return;
  }

  Collection *collection = static_cast<Collection *>(collection_ptr.data);

  Block *block = layout->block();

  AbstractTreeView *tree_view = block_add_view(
      *block,
      "Light Linking Collection Tree View",
      std::make_unique<light_linking::CollectionView>(
          *context_layout, *CTX_data_main(C), *collection));
  tree_view->set_context_menu_title("Light Linking");
  tree_view->set_default_rows(5);

  TreeViewBuilder::build_tree_view(*C, *tree_view, *layout);
}

}  // namespace blender::ui
