/* SPDX-License-Identifier: GPL-2.0-or-later
 * Copyright 2004 Blender Foundation */

/** \file
 * \ingroup spoutliner
 */

#include <cstdio>

#include "DNA_armature_types.h"
#include "DNA_layer_types.h"
#include "DNA_outliner_types.h"
#include "DNA_screen_types.h"
#include "DNA_sequence_types.h"
#include "DNA_space_types.h"

#include "BLI_compiler_compat.h"
#include "BLI_ghash.h"
#include "BLI_listbase.h"

#include "BKE_armature.h"
#include "BKE_context.h"
#include "BKE_layer.h"
#include "BKE_main.h"

#include "DEG_depsgraph.h"

#include "ED_armature.h"
#include "ED_object.h"
#include "ED_outliner.h"

#include "SEQ_select.h"

#include "WM_api.h"
#include "WM_types.h"

#include "outliner_intern.hh"

void ED_outliner_select_sync_from_object_tag(bContext *C)
{
  wmWindowManager *wm = CTX_wm_manager(C);
  wm->outliner_sync_select_dirty |= WM_OUTLINER_SYNC_SELECT_FROM_OBJECT;
}

void ED_outliner_select_sync_from_edit_bone_tag(bContext *C)
{
  wmWindowManager *wm = CTX_wm_manager(C);
  wm->outliner_sync_select_dirty |= WM_OUTLINER_SYNC_SELECT_FROM_EDIT_BONE;
}

void ED_outliner_select_sync_from_pose_bone_tag(bContext *C)
{
  wmWindowManager *wm = CTX_wm_manager(C);
  wm->outliner_sync_select_dirty |= WM_OUTLINER_SYNC_SELECT_FROM_POSE_BONE;
}

void ED_outliner_select_sync_from_sequence_tag(bContext *C)
{
  wmWindowManager *wm = CTX_wm_manager(C);
  wm->outliner_sync_select_dirty |= WM_OUTLINER_SYNC_SELECT_FROM_SEQUENCE;
}

void ED_outliner_select_sync_from_all_tag(bContext *C)
{
  wmWindowManager *wm = CTX_wm_manager(C);
  wm->outliner_sync_select_dirty |= WM_OUTLINER_SYNC_SELECT_FROM_ALL;
}

bool ED_outliner_select_sync_is_dirty(const bContext *C)
{
  wmWindowManager *wm = CTX_wm_manager(C);
  return wm->outliner_sync_select_dirty & WM_OUTLINER_SYNC_SELECT_FROM_ALL;
}

void ED_outliner_select_sync_flag_outliners(const bContext *C)
{
  Main *bmain = CTX_data_main(C);
  wmWindowManager *wm = CTX_wm_manager(C);

  for (bScreen *screen = static_cast<bScreen *>(bmain->screens.first); screen;
       screen = static_cast<bScreen *>(screen->id.next))
  {
    LISTBASE_FOREACH (ScrArea *, area, &screen->areabase) {
      LISTBASE_FOREACH (SpaceLink *, sl, &area->spacedata) {
        if (sl->spacetype == SPACE_OUTLINER) {
          SpaceOutliner *space_outliner = (SpaceOutliner *)sl;

          space_outliner->sync_select_dirty |= wm->outliner_sync_select_dirty;
        }
      }
    }
  }

  /* Clear global sync flag */
  wm->outliner_sync_select_dirty = 0;
}

namespace blender::ed::outliner {

/**
 * Outliner sync select dirty flags are not enough to determine which types to sync,
 * outliner display mode also needs to be considered. This stores the types of data
 * to sync to increase code clarity.
 */
struct SyncSelectTypes {
  bool object;
  bool edit_bone;
  bool pose_bone;
  bool sequence;
};

/**
 * Set which types of data to sync when syncing selection from the outliner based on object
 * interaction mode and outliner display mode
 */
static void outliner_sync_select_from_outliner_set_types(bContext *C,
                                                         SpaceOutliner *space_outliner,
                                                         SyncSelectTypes *sync_types)
{
  TreeViewContext tvc;
  outliner_viewcontext_init(C, &tvc);

  const bool sequence_view = space_outliner->outlinevis == SO_SEQUENCE;

  sync_types->object = !sequence_view;
  sync_types->edit_bone = !sequence_view && (tvc.ob_edit && tvc.ob_edit->type == OB_ARMATURE);
  sync_types->pose_bone = !sequence_view && (tvc.ob_pose && tvc.ob_pose->mode == OB_MODE_POSE);
  sync_types->sequence = sequence_view;
}

/**
 * Current dirty flags and outliner display mode determine which type of syncing should occur.
 * This is to ensure sync flag data is not lost on sync in the wrong display mode.
 * Returns true if a sync is needed.
 */
static bool outliner_sync_select_to_outliner_set_types(const bContext *C,
                                                       SpaceOutliner *space_outliner,
                                                       SyncSelectTypes *sync_types)
{
  TreeViewContext tvc;
  outliner_viewcontext_init(C, &tvc);

  const bool sequence_view = space_outliner->outlinevis == SO_SEQUENCE;

  sync_types->object = !sequence_view &&
                       (space_outliner->sync_select_dirty & WM_OUTLINER_SYNC_SELECT_FROM_OBJECT);
  sync_types->edit_bone = !sequence_view && (tvc.ob_edit && tvc.ob_edit->type == OB_ARMATURE) &&
                          (space_outliner->sync_select_dirty &
                           WM_OUTLINER_SYNC_SELECT_FROM_EDIT_BONE);
  sync_types->pose_bone = !sequence_view && (tvc.ob_pose && tvc.ob_pose->mode == OB_MODE_POSE) &&
                          (space_outliner->sync_select_dirty &
                           WM_OUTLINER_SYNC_SELECT_FROM_POSE_BONE);
  sync_types->sequence = sequence_view && (space_outliner->sync_select_dirty &
                                           WM_OUTLINER_SYNC_SELECT_FROM_SEQUENCE);

  return sync_types->object || sync_types->edit_bone || sync_types->pose_bone ||
         sync_types->sequence;
}

/**
 * Stores items selected from a sync from the outliner. Prevents syncing the selection
 * state of the last instance of an object linked in multiple collections.
 */
struct SelectedItems {
  GSet *objects;
  GSet *edit_bones;
  GSet *pose_bones;
};

static void selected_items_init(SelectedItems *selected_items)
{
  selected_items->objects = BLI_gset_new(BLI_ghashutil_ptrhash, BLI_ghashutil_ptrcmp, __func__);
  selected_items->edit_bones = BLI_gset_new(BLI_ghashutil_ptrhash, BLI_ghashutil_ptrcmp, __func__);
  selected_items->pose_bones = BLI_gset_new(BLI_ghashutil_ptrhash, BLI_ghashutil_ptrcmp, __func__);
}

static void selected_items_free(SelectedItems *selected_items)
{
  BLI_gset_free(selected_items->objects, nullptr);
  BLI_gset_free(selected_items->edit_bones, nullptr);
  BLI_gset_free(selected_items->pose_bones, nullptr);
}

/* Check if an instance of this object been selected by the sync */
static bool is_object_selected(GSet *selected_objects, Base *base)
{
  return BLI_gset_haskey(selected_objects, base);
}

/* Check if an instance of this edit bone been selected by the sync */
static bool is_edit_bone_selected(GSet *selected_ebones, EditBone *ebone)
{
  return BLI_gset_haskey(selected_ebones, ebone);
}

/* Check if an instance of this pose bone been selected by the sync */
static bool is_pose_bone_selected(GSet *selected_pbones, bPoseChannel *pchan)
{
  return BLI_gset_haskey(selected_pbones, pchan);
}

/* Add element's data to selected item set */
static void add_selected_item(GSet *selected, void *data)
{
  BLI_gset_add(selected, data);
}

static void outliner_select_sync_to_object(ViewLayer *view_layer,
                                           TreeElement *te,
                                           TreeStoreElem *tselem,
                                           GSet *selected_objects)
{
  Object *ob = (Object *)tselem->id;
  Base *base = (te->directdata) ? (Base *)te->directdata :
                                  BKE_view_layer_base_find(view_layer, ob);

  if (base && (base->flag & BASE_SELECTABLE)) {
    if (tselem->flag & TSE_SELECTED) {
      ED_object_base_select(base, BA_SELECT);

      add_selected_item(selected_objects, base);
    }
    else if (!is_object_selected(selected_objects, base)) {
      ED_object_base_select(base, BA_DESELECT);
    }
  }
}

static void outliner_select_sync_to_edit_bone(const Scene *scene,
                                              ViewLayer *view_layer,
                                              TreeElement *te,
                                              TreeStoreElem *tselem,
                                              GSet *selected_ebones)
{
  bArmature *arm = (bArmature *)tselem->id;
  EditBone *ebone = (EditBone *)te->directdata;

  short bone_flag = ebone->flag;

  if (EBONE_SELECTABLE(arm, ebone)) {
    if (tselem->flag & TSE_SELECTED) {
      ED_armature_ebone_select_set(ebone, true);
      add_selected_item(selected_ebones, ebone);
    }
    else if (!is_edit_bone_selected(selected_ebones, ebone)) {
      /* Don't flush to parent bone tip, synced selection is iterating the whole tree so
       * deselecting potential children with `ED_armature_ebone_select_set(ebone, false)`
       * would leave own tip deselected. */
      ebone->flag &= ~(BONE_SELECTED | BONE_TIPSEL | BONE_ROOTSEL);
    }
  }

  /* Tag if selection changed */
  if (bone_flag != ebone->flag) {
    BKE_view_layer_synced_ensure(scene, view_layer);
    Object *obedit = BKE_view_layer_edit_object_get(view_layer);
    DEG_id_tag_update(&arm->id, ID_RECALC_SELECT);
    WM_main_add_notifier(NC_OBJECT | ND_BONE_SELECT, obedit);
  }
}

static void outliner_select_sync_to_pose_bone(TreeElement *te,
                                              TreeStoreElem *tselem,
                                              GSet *selected_pbones)
{
  Object *ob = (Object *)tselem->id;
  bArmature *arm = static_cast<bArmature *>(ob->data);
  bPoseChannel *pchan = (bPoseChannel *)te->directdata;

  short bone_flag = pchan->bone->flag;

  if (PBONE_SELECTABLE(arm, pchan->bone)) {
    if (tselem->flag & TSE_SELECTED) {
      pchan->bone->flag |= BONE_SELECTED;

      add_selected_item(selected_pbones, pchan);
    }
    else if (!is_pose_bone_selected(selected_pbones, pchan)) {
      pchan->bone->flag &= ~BONE_SELECTED;
    }
  }

  /* Tag if selection changed */
  if (bone_flag != pchan->bone->flag) {
    DEG_id_tag_update(&arm->id, ID_RECALC_SELECT);
    WM_main_add_notifier(NC_OBJECT | ND_BONE_SELECT, ob);
  }
}

static void outliner_select_sync_to_sequence(Scene *scene, TreeStoreElem *tselem)
{
  Sequence *seq = (Sequence *)tselem->id;

  if (tselem->flag & TSE_ACTIVE) {
    SEQ_select_active_set(scene, seq);
  }

  if (tselem->flag & TSE_SELECTED) {
    seq->flag |= SELECT;
  }
  else {
    seq->flag &= ~SELECT;
  }
}

/** Sync select and active flags from outliner to active view layer, bones, and sequencer. */
static void outliner_sync_selection_from_outliner(Scene *scene,
                                                  ViewLayer *view_layer,
                                                  ListBase *tree,
                                                  const SyncSelectTypes *sync_types,
                                                  SelectedItems *selected_items)
{

  LISTBASE_FOREACH (TreeElement *, te, tree) {
    TreeStoreElem *tselem = TREESTORE(te);

    if ((tselem->type == TSE_SOME_ID) && (te->idcode == ID_OB)) {
      if (sync_types->object) {
        outliner_select_sync_to_object(view_layer, te, tselem, selected_items->objects);
      }
    }
    else if (tselem->type == TSE_EBONE) {
      if (sync_types->edit_bone) {
        outliner_select_sync_to_edit_bone(
            scene, view_layer, te, tselem, selected_items->edit_bones);
      }
    }
    else if (tselem->type == TSE_POSE_CHANNEL) {
      if (sync_types->pose_bone) {
        outliner_select_sync_to_pose_bone(te, tselem, selected_items->pose_bones);
      }
    }
    else if (tselem->type == TSE_SEQUENCE) {
      if (sync_types->sequence) {
        outliner_select_sync_to_sequence(scene, tselem);
      }
    }

    outliner_sync_selection_from_outliner(
        scene, view_layer, &te->subtree, sync_types, selected_items);
  }
}

}  // namespace blender::ed::outliner

void ED_outliner_select_sync_from_outliner(bContext *C, SpaceOutliner *space_outliner)
{
  using namespace blender::ed::outliner;

  /* Don't sync if not checked or in certain outliner display modes */
  if (!(space_outliner->flag & SO_SYNC_SELECT) || ELEM(space_outliner->outlinevis,
                                                       SO_LIBRARIES,
                                                       SO_OVERRIDES_LIBRARY,
                                                       SO_DATA_API,
                                                       SO_ID_ORPHANS))
  {
    return;
  }

  Scene *scene = CTX_data_scene(C);
  ViewLayer *view_layer = CTX_data_view_layer(C);

  SyncSelectTypes sync_types;
  outliner_sync_select_from_outliner_set_types(C, space_outliner, &sync_types);

  /* To store elements that have been selected to prevent linked object sync errors */
  SelectedItems selected_items;

  selected_items_init(&selected_items);

  outliner_sync_selection_from_outliner(
      scene, view_layer, &space_outliner->tree, &sync_types, &selected_items);

  selected_items_free(&selected_items);

  /* Tag for updates and clear dirty flag to prevent a sync to the outliner on draw. */
  if (sync_types.object) {
    space_outliner->sync_select_dirty &= ~WM_OUTLINER_SYNC_SELECT_FROM_OBJECT;
    DEG_id_tag_update(&scene->id, ID_RECALC_SELECT);
    WM_event_add_notifier(C, NC_SCENE | ND_OB_SELECT, scene);
  }
  else if (sync_types.edit_bone) {
    space_outliner->sync_select_dirty &= ~WM_OUTLINER_SYNC_SELECT_FROM_EDIT_BONE;
  }
  else if (sync_types.pose_bone) {
    space_outliner->sync_select_dirty &= ~WM_OUTLINER_SYNC_SELECT_FROM_POSE_BONE;
  }
  if (sync_types.sequence) {
    space_outliner->sync_select_dirty &= ~WM_OUTLINER_SYNC_SELECT_FROM_SEQUENCE;
    WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER | NA_SELECTED, scene);
  }
}

namespace blender::ed::outliner {

static void outliner_select_sync_from_object(const Scene *scene,
                                             ViewLayer *view_layer,
                                             Object *obact,
                                             TreeElement *te,
                                             TreeStoreElem *tselem)
{
  Object *ob = (Object *)tselem->id;
  BKE_view_layer_synced_ensure(scene, view_layer);
  Base *base = (te->directdata) ? (Base *)te->directdata :
                                  BKE_view_layer_base_find(view_layer, ob);
  const bool is_selected = (base != nullptr) && ((base->flag & BASE_SELECTED) != 0);

  if (base && (ob == obact)) {
    tselem->flag |= TSE_ACTIVE;
  }
  else {
    tselem->flag &= ~TSE_ACTIVE;
  }

  if (is_selected) {
    tselem->flag |= TSE_SELECTED;
  }
  else {
    tselem->flag &= ~TSE_SELECTED;
  }
}

static void outliner_select_sync_from_edit_bone(EditBone *ebone_active,
                                                TreeElement *te,
                                                TreeStoreElem *tselem)
{
  EditBone *ebone = (EditBone *)te->directdata;

  if (ebone == ebone_active) {
    tselem->flag |= TSE_ACTIVE;
  }
  else {
    tselem->flag &= ~TSE_ACTIVE;
  }

  if (ebone->flag & BONE_SELECTED) {
    tselem->flag |= TSE_SELECTED;
  }
  else {
    tselem->flag &= ~TSE_SELECTED;
  }
}

static void outliner_select_sync_from_pose_bone(bPoseChannel *pchan_active,
                                                TreeElement *te,
                                                TreeStoreElem *tselem)
{
  bPoseChannel *pchan = (bPoseChannel *)te->directdata;
  Bone *bone = pchan->bone;

  if (pchan == pchan_active) {
    tselem->flag |= TSE_ACTIVE;
  }
  else {
    tselem->flag &= ~TSE_ACTIVE;
  }

  if (bone->flag & BONE_SELECTED) {
    tselem->flag |= TSE_SELECTED;
  }
  else {
    tselem->flag &= ~TSE_SELECTED;
  }
}

static void outliner_select_sync_from_sequence(Sequence *sequence_active, TreeStoreElem *tselem)
{
  Sequence *seq = (Sequence *)tselem->id;

  if (seq == sequence_active) {
    tselem->flag |= TSE_ACTIVE;
  }
  else {
    tselem->flag &= ~TSE_ACTIVE;
  }

  if (seq->flag & SELECT) {
    tselem->flag |= TSE_SELECTED;
  }
  else {
    tselem->flag &= ~TSE_SELECTED;
  }
}

/**
 * Contains active object, bones, and sequence for syncing to prevent getting active data
 * repeatedly throughout syncing to the outliner.
 */
struct SyncSelectActiveData {
  Object *object;
  EditBone *edit_bone;
  bPoseChannel *pose_channel;
  Sequence *sequence;
};

/** Sync select and active flags from active view layer, bones, and sequences to the outliner. */
static void outliner_sync_selection_to_outliner(const Scene *scene,
                                                ViewLayer *view_layer,
                                                SpaceOutliner *space_outliner,
                                                ListBase *tree,
                                                SyncSelectActiveData *active_data,
                                                const SyncSelectTypes *sync_types)
{
  LISTBASE_FOREACH (TreeElement *, te, tree) {
    TreeStoreElem *tselem = TREESTORE(te);

    if ((tselem->type == TSE_SOME_ID) && te->idcode == ID_OB) {
      if (sync_types->object) {
        outliner_select_sync_from_object(scene, view_layer, active_data->object, te, tselem);
      }
    }
    else if (tselem->type == TSE_EBONE) {
      if (sync_types->edit_bone) {
        outliner_select_sync_from_edit_bone(active_data->edit_bone, te, tselem);
      }
    }
    else if (tselem->type == TSE_POSE_CHANNEL) {
      if (sync_types->pose_bone) {
        outliner_select_sync_from_pose_bone(active_data->pose_channel, te, tselem);
      }
    }
    else if (tselem->type == TSE_SEQUENCE) {
      if (sync_types->sequence) {
        outliner_select_sync_from_sequence(active_data->sequence, tselem);
      }
    }
    else {
      tselem->flag &= ~(TSE_SELECTED | TSE_ACTIVE);
    }

    /* Sync subtree elements */
    outliner_sync_selection_to_outliner(
        scene, view_layer, space_outliner, &te->subtree, active_data, sync_types);
  }
}

/* Get active data from context */
static void get_sync_select_active_data(const bContext *C, SyncSelectActiveData *active_data)
{
  Scene *scene = CTX_data_scene(C);
  ViewLayer *view_layer = CTX_data_view_layer(C);
  BKE_view_layer_synced_ensure(scene, view_layer);
  active_data->object = BKE_view_layer_active_object_get(view_layer);
  active_data->edit_bone = CTX_data_active_bone(C);
  active_data->pose_channel = CTX_data_active_pose_bone(C);
  active_data->sequence = SEQ_select_active_get(scene);
}

void outliner_sync_selection(const bContext *C, SpaceOutliner *space_outliner)
{
  /* Set which types of data to sync from sync dirty flag and outliner display mode */
  SyncSelectTypes sync_types;
  const bool sync_required = outliner_sync_select_to_outliner_set_types(
      C, space_outliner, &sync_types);

  if (sync_required) {
    const Scene *scene = CTX_data_scene(C);
    ViewLayer *view_layer = CTX_data_view_layer(C);

    /* Store active object, bones, and sequence */
    SyncSelectActiveData active_data;
    get_sync_select_active_data(C, &active_data);

    outliner_sync_selection_to_outliner(
        scene, view_layer, space_outliner, &space_outliner->tree, &active_data, &sync_types);

    /* Keep any un-synced data in the dirty flag. */
    if (sync_types.object) {
      space_outliner->sync_select_dirty &= ~WM_OUTLINER_SYNC_SELECT_FROM_OBJECT;
    }
    if (sync_types.edit_bone) {
      space_outliner->sync_select_dirty &= ~WM_OUTLINER_SYNC_SELECT_FROM_EDIT_BONE;
    }
    if (sync_types.pose_bone) {
      space_outliner->sync_select_dirty &= ~WM_OUTLINER_SYNC_SELECT_FROM_POSE_BONE;
    }
    if (sync_types.sequence) {
      space_outliner->sync_select_dirty &= ~WM_OUTLINER_SYNC_SELECT_FROM_SEQUENCE;
    }
  }
}

}  // namespace blender::ed::outliner
