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

/** \file
 * \ingroup edanimation
 */

#include <cmath>

#include "MEM_guardedalloc.h"

#include "DNA_object_types.h"
#include "DNA_scene_types.h"

#include "BLI_blenlib.h"
#include "BLI_utildefines.h"

#include "BLT_translation.hh"

#include "BKE_context.hh"
#include "BKE_idprop.hh"
#include "BKE_layer.hh"
#include "BKE_main.hh"
#include "BKE_report.hh"
#include "BKE_scene.hh"
#include "BKE_screen.hh"
#include "BKE_unit.hh"

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

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

#include "GPU_immediate.hh"
#include "GPU_matrix.hh"
#include "GPU_state.hh"

#include "UI_interface.hh"
#include "UI_interface_icons.hh"
#include "UI_resources.hh"
#include "UI_view2d.hh"

#include "ED_anim_api.hh"
#include "ED_keyframes_edit.hh"
#include "ED_markers.hh"
#include "ED_numinput.hh"
#include "ED_object.hh"
#include "ED_screen.hh"
#include "ED_select_utils.hh"
#include "ED_transform.hh"
#include "ED_util.hh"

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

/* -------------------------------------------------------------------- */
/** \name Marker API
 * \{ */

/* helper function for getting the list of markers to work on */
static ListBase *context_get_markers(Scene *scene, ScrArea *area)
{
  /* local marker sets... */
  if (area) {
    if (area->spacetype == SPACE_ACTION) {
      SpaceAction *saction = (SpaceAction *)area->spacedata.first;

      /* local markers can only be shown when there's only a single active action to grab them from
       * - flag only takes effect when there's an action, otherwise it can get too confusing?
       */
      if (ELEM(saction->mode, SACTCONT_ACTION, SACTCONT_SHAPEKEY) && (saction->action)) {
        if (saction->flag & SACTION_POSEMARKERS_SHOW) {
          return &saction->action->markers;
        }
      }
    }
  }

  /* default to using the scene's markers */
  return &scene->markers;
}

/* ............. */

ListBase *ED_context_get_markers(const bContext *C)
{
  return context_get_markers(CTX_data_scene(C), CTX_wm_area(C));
}

ListBase *ED_animcontext_get_markers(const bAnimContext *ac)
{
  if (ac) {
    return context_get_markers(ac->scene, ac->area);
  }
  return nullptr;
}

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

int ED_markers_post_apply_transform(
    ListBase *markers, Scene *scene, int mode, float value, char side)
{
  float cfra = float(scene->r.cfra);
  int changed_tot = 0;

  /* sanity check - no markers, or locked markers */
  if ((scene->toolsettings->lock_markers) || (markers == nullptr)) {
    return changed_tot;
  }

  /* affect selected markers - it's unlikely that we will want to affect all in this way? */
  LISTBASE_FOREACH (TimeMarker *, marker, markers) {
    if (marker->flag & SELECT) {
      switch (mode) {
        case TFM_TIME_TRANSLATE:
        case TFM_TIME_EXTEND: {
          /* apply delta if marker is on the right side of the current frame */
          if ((side == 'B') || (side == 'L' && marker->frame < cfra) ||
              (side == 'R' && marker->frame >= cfra))
          {
            marker->frame += round_fl_to_int(value);
            changed_tot++;
          }
          break;
        }
        case TFM_TIME_SCALE: {
          /* rescale the distance between the marker and the current frame */
          marker->frame = cfra + round_fl_to_int(float(marker->frame - cfra) * value);
          changed_tot++;
          break;
        }
      }
    }
  }

  return changed_tot;
}

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

TimeMarker *ED_markers_find_nearest_marker(ListBase *markers, float x)
{
  TimeMarker *nearest = nullptr;
  float dist, min_dist = 1000000;

  if (markers) {
    LISTBASE_FOREACH (TimeMarker *, marker, markers) {
      dist = fabsf(float(marker->frame) - x);

      if (dist < min_dist) {
        min_dist = dist;
        nearest = marker;
      }
    }
  }

  return nearest;
}

int ED_markers_find_nearest_marker_time(ListBase *markers, float x)
{
  TimeMarker *nearest = ED_markers_find_nearest_marker(markers, x);
  return (nearest) ? (nearest->frame) : round_fl_to_int(x);
}

void ED_markers_get_minmax(ListBase *markers, short sel, float *r_first, float *r_last)
{
  float min, max;

  /* sanity check */
  // printf("markers = %p -  %p, %p\n", markers, markers->first, markers->last);
  if (ELEM(nullptr, markers, markers->first, markers->last)) {
    *r_first = 0.0f;
    *r_last = 0.0f;
    return;
  }

  min = FLT_MAX;
  max = -FLT_MAX;
  LISTBASE_FOREACH (TimeMarker *, marker, markers) {
    if (!sel || (marker->flag & SELECT)) {
      if (marker->frame < min) {
        min = float(marker->frame);
      }
      if (marker->frame > max) {
        max = float(marker->frame);
      }
    }
  }

  /* set the min/max values */
  *r_first = min;
  *r_last = max;
}

/**
 * Function used in operator polls, checks whether the markers region is currently drawn in the
 * editor in which the operator is called.
 */
static bool ED_operator_markers_region_active(bContext *C)
{
  ScrArea *area = CTX_wm_area(C);
  if (area == nullptr) {
    return false;
  }

  switch (area->spacetype) {
    case SPACE_ACTION: {
      SpaceAction *saction = static_cast<SpaceAction *>(area->spacedata.first);
      if (saction->flag & SACTION_SHOW_MARKERS) {
        return true;
      }
      break;
    }
    case SPACE_GRAPH: {
      SpaceGraph *sipo = static_cast<SpaceGraph *>(area->spacedata.first);
      if (sipo->mode != SIPO_MODE_DRIVERS && sipo->flag & SIPO_SHOW_MARKERS) {
        return true;
      }
      break;
    }
    case SPACE_NLA: {
      SpaceNla *snla = static_cast<SpaceNla *>(area->spacedata.first);
      if (snla->flag & SNLA_SHOW_MARKERS) {
        return true;
      }
      break;
    }
    case SPACE_SEQ: {
      SpaceSeq *seq = static_cast<SpaceSeq *>(area->spacedata.first);
      if (seq->flag & SEQ_SHOW_MARKERS) {
        return true;
      }
      break;
    }
  }
  return false;
}

static bool region_position_is_over_marker(View2D *v2d, ListBase *markers, float region_x)
{
  if (markers == nullptr || BLI_listbase_is_empty(markers)) {
    return false;
  }

  float frame_at_position = UI_view2d_region_to_view_x(v2d, region_x);
  TimeMarker *nearest_marker = ED_markers_find_nearest_marker(markers, frame_at_position);
  float pixel_distance = UI_view2d_scale_get_x(v2d) *
                         fabsf(nearest_marker->frame - frame_at_position);

  return pixel_distance <= UI_ICON_SIZE;
}

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

/* Adds a marker to list of cfra elems */
static void add_marker_to_cfra_elem(ListBase *lb, TimeMarker *marker, short only_sel)
{
  CfraElem *ce, *cen;

  /* should this one only be considered if it is selected? */
  if ((only_sel) && ((marker->flag & SELECT) == 0)) {
    return;
  }

  /* insertion sort - try to find a previous cfra elem */
  for (ce = static_cast<CfraElem *>(lb->first); ce; ce = ce->next) {
    if (ce->cfra == marker->frame) {
      /* do because of double keys */
      if (marker->flag & SELECT) {
        ce->sel = marker->flag;
      }
      return;
    }
    if (ce->cfra > marker->frame) {
      break;
    }
  }

  cen = static_cast<CfraElem *>(MEM_callocN(sizeof(CfraElem), "add_to_cfra_elem"));
  if (ce) {
    BLI_insertlinkbefore(lb, ce, cen);
  }
  else {
    BLI_addtail(lb, cen);
  }

  cen->cfra = marker->frame;
  cen->sel = marker->flag;
}

void ED_markers_make_cfra_list(ListBase *markers, ListBase *lb, short only_sel)
{
  if (lb) {
    /* Clear the list first, since callers have no way of knowing
     * whether this terminated early otherwise. This may lead
     * to crashes if the user didn't clear the memory first.
     */
    lb->first = lb->last = nullptr;
  }
  else {
    return;
  }

  if (markers == nullptr) {
    return;
  }

  LISTBASE_FOREACH (TimeMarker *, marker, markers) {
    add_marker_to_cfra_elem(lb, marker, only_sel);
  }
}

void ED_markers_deselect_all(ListBase *markers, int action)
{
  if (action == SEL_TOGGLE) {
    action = ED_markers_get_first_selected(markers) ? SEL_DESELECT : SEL_SELECT;
  }

  LISTBASE_FOREACH (TimeMarker *, marker, markers) {
    if (action == SEL_SELECT) {
      marker->flag |= SELECT;
    }
    else if (action == SEL_DESELECT) {
      marker->flag &= ~SELECT;
    }
    else if (action == SEL_INVERT) {
      marker->flag ^= SELECT;
    }
    else {
      BLI_assert(0);
    }
  }
}

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

TimeMarker *ED_markers_get_first_selected(ListBase *markers)
{
  if (markers) {
    LISTBASE_FOREACH (TimeMarker *, marker, markers) {
      if (marker->flag & SELECT) {
        return marker;
      }
    }
  }

  return nullptr;
}

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

void debug_markers_print_list(ListBase *markers)
{
  /* NOTE: do NOT make static or use `ifdef`'s as "unused code".
   * That's too much trouble when we need to use for quick debugging! */
  if (markers == nullptr) {
    printf("No markers list to print debug for\n");
    return;
  }

  printf("List of markers follows: -----\n");

  LISTBASE_FOREACH (TimeMarker *, marker, markers) {
    printf(
        "\t'%s' on %d at %p with %u\n", marker->name, marker->frame, (void *)marker, marker->flag);
  }

  printf("End of list ------------------\n");
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Marker Drawing
 * \{ */

static void marker_color_get(const TimeMarker *marker, uchar *r_text_color, uchar *r_line_color)
{
  if (marker->flag & SELECT) {
    UI_GetThemeColor4ubv(TH_TEXT_HI, r_text_color);
    UI_GetThemeColor4ubv(TH_TIME_MARKER_LINE_SELECTED, r_line_color);
  }
  else {
    UI_GetThemeColor4ubv(TH_TEXT, r_text_color);
    UI_GetThemeColor4ubv(TH_TIME_MARKER_LINE, r_line_color);
  }
}

static void draw_marker_name(const uchar *text_color,
                             const uiFontStyle *fstyle,
                             TimeMarker *marker,
                             float marker_x,
                             float xmax,
                             float text_y)
{
  const char *name = marker->name;
  uchar final_text_color[4];

  copy_v4_v4_uchar(final_text_color, text_color);

#ifdef DURIAN_CAMERA_SWITCH
  if (marker->camera) {
    Object *camera = marker->camera;
    name = camera->id.name + 2;
    if (camera->visibility_flag & OB_HIDE_RENDER) {
      final_text_color[3] = 100;
    }
  }
#endif

  const int icon_half_width = UI_ICON_SIZE * 0.6;
  uiFontStyleDraw_Params fs_params{};
  fs_params.align = UI_STYLE_TEXT_LEFT;
  fs_params.word_wrap = 0;

  rcti rect{};
  rect.xmin = marker_x + icon_half_width;
  rect.xmax = xmax - icon_half_width;
  rect.ymin = text_y;
  rect.ymax = text_y;

  UI_fontstyle_draw(fstyle, &rect, name, strlen(name), final_text_color, &fs_params);
}

static void draw_marker_line(const uchar *color, int xpos, int ymin, int ymax)
{
  GPUVertFormat *format = immVertexFormat();
  uint pos = GPU_vertformat_attr_add(format, "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT);

  immBindBuiltinProgram(GPU_SHADER_3D_LINE_DASHED_UNIFORM_COLOR);

  float viewport_size[4];
  GPU_viewport_size_get_f(viewport_size);
  immUniform2f("viewport_size", viewport_size[2] / UI_SCALE_FAC, viewport_size[3] / UI_SCALE_FAC);

  immUniformColor4ubv(color);
  immUniform1i("colors_len", 0); /* "simple" mode */
  immUniform1f("dash_width", 6.0f);
  immUniform1f("udash_factor", 0.5f);

  immBegin(GPU_PRIM_LINES, 2);
  immVertex2f(pos, xpos, ymin);
  immVertex2f(pos, xpos, ymax);
  immEnd();

  immUnbindProgram();
}

static int marker_get_icon_id(TimeMarker *marker, int flag)
{
  if (flag & DRAW_MARKERS_LOCAL) {
    return (marker->flag & SELECT) ? ICON_PMARKER_SEL : ICON_PMARKER;
  }
#ifdef DURIAN_CAMERA_SWITCH
  if (marker->camera) {
    return (marker->flag & SELECT) ? ICON_OUTLINER_OB_CAMERA : ICON_CAMERA_DATA;
  }
#endif
  return (marker->flag & SELECT) ? ICON_MARKER_HLT : ICON_MARKER;
}

static void draw_marker(const uiFontStyle *fstyle,
                        TimeMarker *marker,
                        int xpos,
                        int xmax,
                        int flag,
                        int region_height,
                        bool is_elevated)
{
  uchar line_color[4], text_color[4];

  marker_color_get(marker, text_color, line_color);

  GPU_blend(GPU_BLEND_ALPHA);

  draw_marker_line(line_color, xpos, UI_SCALE_FAC * 28, region_height);

  int icon_id = marker_get_icon_id(marker, flag);

  uchar marker_color[4];
  if (marker->flag & SELECT) {
    UI_GetThemeColor4ubv(TH_TIME_MARKER_LINE_SELECTED, marker_color);
  }
  else {
    UI_GetThemeColor4ubv(TH_TEXT, marker_color);
  }

  UI_icon_draw_ex(xpos - (0.5f * UI_ICON_SIZE) - (0.5f * U.pixelsize),
                  UI_SCALE_FAC * 18,
                  icon_id,
                  UI_INV_SCALE_FAC,
                  1.0f,
                  0.0f,
                  marker_color,
                  false,
                  UI_NO_ICON_OVERLAY_TEXT);

  GPU_blend(GPU_BLEND_NONE);

  float name_y = UI_SCALE_FAC * 18;
  /* Give an offset to the marker that is elevated. */
  if (is_elevated) {
    name_y += UI_SCALE_FAC * 10;
  }
  draw_marker_name(text_color, fstyle, marker, xpos, xmax, name_y);
}

static void draw_markers_background(const rctf *rect)
{
  uint pos = GPU_vertformat_attr_add(immVertexFormat(), "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT);
  immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR);

  uchar shade[4];
  UI_GetThemeColor4ubv(TH_TIME_SCRUB_BACKGROUND, shade);

  immUniformColor4ubv(shade);

  GPU_blend(GPU_BLEND_ALPHA);

  immRectf(pos, rect->xmin, rect->ymin, rect->xmax, rect->ymax);

  GPU_blend(GPU_BLEND_NONE);

  immUnbindProgram();
}

static bool marker_is_in_frame_range(TimeMarker *marker, const int frame_range[2])
{
  if (marker->frame < frame_range[0]) {
    return false;
  }
  if (marker->frame > frame_range[1]) {
    return false;
  }
  return true;
}

static void get_marker_region_rect(View2D *v2d, rctf *rect)
{
  rect->xmin = v2d->cur.xmin;
  rect->xmax = v2d->cur.xmax;
  rect->ymin = 0;
  rect->ymax = UI_MARKER_MARGIN_Y;
}

static void get_marker_clip_frame_range(View2D *v2d, float xscale, int r_range[2])
{
  float font_width_max = (10 * UI_SCALE_FAC) / xscale;
  r_range[0] = v2d->cur.xmin - sizeof(TimeMarker::name) * font_width_max;
  r_range[1] = v2d->cur.xmax + font_width_max;
}

static int markers_frame_sort(const void *a, const void *b)
{
  const TimeMarker *marker_a = static_cast<const TimeMarker *>(a);
  const TimeMarker *marker_b = static_cast<const TimeMarker *>(b);

  return marker_a->frame > marker_b->frame;
}

void ED_markers_draw(const bContext *C, int flag)
{
  ListBase *markers = ED_context_get_markers(C);
  if (markers == nullptr || BLI_listbase_is_empty(markers)) {
    return;
  }

  ARegion *region = CTX_wm_region(C);
  View2D *v2d = UI_view2d_fromcontext(C);
  int cfra = CTX_data_scene(C)->r.cfra;

  GPU_line_width(1.0f);

  rctf markers_region_rect;
  get_marker_region_rect(v2d, &markers_region_rect);

  draw_markers_background(&markers_region_rect);

  /* no time correction for framelen! space is drawn with old values */
  float xscale, dummy;
  UI_view2d_scale_get(v2d, &xscale, &dummy);
  GPU_matrix_push();
  GPU_matrix_scale_2f(1.0f / xscale, 1.0f);

  int clip_frame_range[2];
  get_marker_clip_frame_range(v2d, xscale, clip_frame_range);

  const uiFontStyle *fstyle = UI_FSTYLE_WIDGET;

  /* Markers are not stored by frame order, so we need to sort it here. */
  ListBase sorted_markers;

  BLI_duplicatelist(&sorted_markers, markers);
  BLI_listbase_sort(&sorted_markers, markers_frame_sort);

  /**
   * Set a temporary bit in the marker's flag to indicate that it should be elevated.
   * This bit will be flipped back at the end of this function.
   */
  const int ELEVATED = 0x10;
  LISTBASE_FOREACH (TimeMarker *, marker, &sorted_markers) {
    const bool is_elevated = (marker->flag & SELECT) ||
                             (cfra >= marker->frame &&
                              (marker->next == nullptr || cfra < marker->next->frame));
    SET_FLAG_FROM_TEST(marker->flag, is_elevated, ELEVATED);
  }

  /* Separate loops in order to draw selected markers on top. */

  /**
   * Draw non-elevated markers first.
   * Note that unlike the elevated markers, these marker names will always be clipped by the
   * proceeding marker. This is done because otherwise, the text overlaps with the icon of the
   * marker itself.
   */
  LISTBASE_FOREACH (TimeMarker *, marker, &sorted_markers) {
    if ((marker->flag & ELEVATED) == 0 && marker_is_in_frame_range(marker, clip_frame_range)) {
      const int xmax = marker->next ? marker->next->frame : clip_frame_range[1] + 1;
      draw_marker(
          fstyle, marker, marker->frame * xscale, xmax * xscale, flag, region->winy, false);
    }
  }

  /* Now draw the elevated markers */
  for (TimeMarker *marker = static_cast<TimeMarker *>(sorted_markers.first); marker != nullptr;) {

    /* Skip this marker if it is elevated or out of the frame range. */
    if ((marker->flag & ELEVATED) == 0 || !marker_is_in_frame_range(marker, clip_frame_range)) {
      marker = marker->next;
      continue;
    }

    /* Find the next elevated marker. */
    /* We use the next marker to determine how wide our text should be */
    TimeMarker *next_marker = marker->next;
    while (next_marker != nullptr && (next_marker->flag & ELEVATED) == 0) {
      next_marker = next_marker->next;
    }

    const int xmax = next_marker ? next_marker->frame : clip_frame_range[1] + 1;
    draw_marker(fstyle, marker, marker->frame * xscale, xmax * xscale, flag, region->winy, true);

    marker = next_marker;
  }

  /* Reset the elevated flag. */
  LISTBASE_FOREACH (TimeMarker *, marker, &sorted_markers) {
    marker->flag &= ~ELEVATED;
  }

  BLI_freelistN(&sorted_markers);

  GPU_matrix_pop();
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Marker Wrappers API
 *
 * These wrappers allow marker operators to function within the confines
 * of standard animation editors, such that they can coexist with the
 * primary operations of those editors.
 * \{ */

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

/* special poll() which checks if there are selected markers first */
static bool ed_markers_poll_selected_markers(bContext *C)
{
  ListBase *markers = ED_context_get_markers(C);

  if (!ED_operator_markers_region_active(C)) {
    return false;
  }

  /* check if some marker is selected */
  if (ED_markers_get_first_selected(markers) == nullptr) {
    CTX_wm_operator_poll_msg_set(C, "No markers are selected");
    return false;
  }

  return true;
}

static bool ed_markers_poll_selected_no_locked_markers(bContext *C)
{
  ListBase *markers = ED_context_get_markers(C);
  ToolSettings *ts = CTX_data_tool_settings(C);

  if (!ED_operator_markers_region_active(C)) {
    return false;
  }

  if (ts->lock_markers) {
    CTX_wm_operator_poll_msg_set(C, "Markers are locked");
    return false;
  }

  /* check if some marker is selected */
  if (ED_markers_get_first_selected(markers) == nullptr) {
    CTX_wm_operator_poll_msg_set(C, "No markers are selected");
    return false;
  }

  return true;
}

/* special poll() which checks if there are any markers at all first */
static bool ed_markers_poll_markers_exist(bContext *C)
{
  ListBase *markers = ED_context_get_markers(C);
  ToolSettings *ts = CTX_data_tool_settings(C);

  if (ts->lock_markers || !ED_operator_markers_region_active(C)) {
    return false;
  }

  /* list of markers must exist, as well as some markers in it! */
  return (markers && markers->first);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Add Markers
 * \{ */

/* add TimeMarker at current frame */
static int ed_marker_add_exec(bContext *C, wmOperator * /*op*/)
{
  ListBase *markers = ED_context_get_markers(C);
  TimeMarker *marker;
  int frame = CTX_data_scene(C)->r.cfra;

  if (markers == nullptr) {
    return OPERATOR_CANCELLED;
  }

  /* prefer not having 2 markers at the same place,
   * though the user can move them to overlap once added */
  LISTBASE_FOREACH (TimeMarker *, marker, markers) {
    if (marker->frame == frame) {
      return OPERATOR_CANCELLED;
    }
  }

  /* deselect all */
  LISTBASE_FOREACH (TimeMarker *, marker, markers) {
    marker->flag &= ~SELECT;
  }

  marker = static_cast<TimeMarker *>(MEM_callocN(sizeof(TimeMarker), "TimeMarker"));
  marker->flag = SELECT;
  marker->frame = frame;
  SNPRINTF(marker->name, "F_%02d", frame);
  BLI_addtail(markers, marker);

  WM_event_add_notifier(C, NC_SCENE | ND_MARKERS, nullptr);
  WM_event_add_notifier(C, NC_ANIMATION | ND_MARKERS, nullptr);

  return OPERATOR_FINISHED;
}

static void MARKER_OT_add(wmOperatorType *ot)
{
  /* identifiers */
  ot->name = "Add Time Marker";
  ot->description = "Add a new time marker";
  ot->idname = "MARKER_OT_add";

  /* api callbacks */
  ot->exec = ed_marker_add_exec;
  ot->poll = ED_operator_markers_region_active;

  /* flags */
  ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Transform Markers
 * \{ */

/* operator state vars used:
 *    frames: delta movement
 *
 * functions:
 *
 *     init()   check selection, add customdata with old values and some lookups
 *
 *     apply()  do the actual movement
 *
 *     exit()    cleanup, send notifier
 *
 *     cancel() to escape from modal
 *
 * callbacks:
 *
 *     exec()    calls init, apply, exit
 *
 *     invoke() calls init, adds modal handler
 *
 *     modal()    accept modal events while doing it, ends with apply and exit, or cancel
 */

struct MarkerMove {
  SpaceLink *slink;
  ListBase *markers;
  short event_type, event_val; /* store invoke-event, to verify */
  int *oldframe, evtx, firstx;
  NumInput num;
};

static bool ed_marker_move_use_time(MarkerMove *mm)
{
  if (((mm->slink->spacetype == SPACE_SEQ) && !(((SpaceSeq *)mm->slink)->flag & SEQ_DRAWFRAMES)) ||
      ((mm->slink->spacetype == SPACE_ACTION) &&
       (((SpaceAction *)mm->slink)->flag & SACTION_DRAWTIME)) ||
      ((mm->slink->spacetype == SPACE_GRAPH) &&
       (((SpaceGraph *)mm->slink)->flag & SIPO_DRAWTIME)) ||
      ((mm->slink->spacetype == SPACE_NLA) && (((SpaceNla *)mm->slink)->flag & SNLA_DRAWTIME)))
  {
    return true;
  }

  return false;
}

static void ed_marker_move_update_header(bContext *C, wmOperator *op)
{
  Scene *scene = CTX_data_scene(C);
  MarkerMove *mm = static_cast<MarkerMove *>(op->customdata);
  TimeMarker *marker, *selmarker = nullptr;
  const int ofs = RNA_int_get(op->ptr, "frames");
  char str[UI_MAX_DRAW_STR];
  char str_ofs[NUM_STR_REP_LEN];
  int totmark;
  const bool use_time = ed_marker_move_use_time(mm);

  for (totmark = 0, marker = static_cast<TimeMarker *>(mm->markers->first); marker;
       marker = marker->next)
  {
    if (marker->flag & SELECT) {
      selmarker = marker;
      totmark++;
    }
  }

  if (hasNumInput(&mm->num)) {
    outputNumInput(&mm->num, str_ofs, &scene->unit);
  }
  else if (use_time) {
    SNPRINTF(str_ofs, "%.2f", FRA2TIME(ofs));
  }
  else {
    SNPRINTF(str_ofs, "%d", ofs);
  }

  if (totmark == 1 && selmarker) {
    /* we print current marker value */
    if (use_time) {
      SNPRINTF(str, IFACE_("Marker %.2f offset %s"), FRA2TIME(selmarker->frame), str_ofs);
    }
    else {
      SNPRINTF(str, IFACE_("Marker %d offset %s"), selmarker->frame, str_ofs);
    }
  }
  else {
    SNPRINTF(str, IFACE_("Marker offset %s"), str_ofs);
  }

  ED_area_status_text(CTX_wm_area(C), str);
}

/* copy selection to temp buffer */
/* return 0 if not OK */
static bool ed_marker_move_init(bContext *C, wmOperator *op)
{
  Scene *scene = CTX_data_scene(C);
  ListBase *markers = ED_context_get_markers(C);
  MarkerMove *mm;
  TimeMarker *marker;
  int a, totmark;

  if (markers == nullptr) {
    return false;
  }

  for (totmark = 0, marker = static_cast<TimeMarker *>(markers->first); marker;
       marker = marker->next)
  {
    if (marker->flag & SELECT) {
      totmark++;
    }
  }

  if (totmark == 0) {
    return false;
  }

  op->customdata = mm = static_cast<MarkerMove *>(MEM_callocN(sizeof(MarkerMove), "Markermove"));
  mm->slink = CTX_wm_space_data(C);
  mm->markers = markers;
  mm->oldframe = static_cast<int *>(MEM_callocN(totmark * sizeof(int), "MarkerMove oldframe"));

  initNumInput(&mm->num);
  mm->num.idx_max = 0; /* one axis */
  mm->num.val_flag[0] |= NUM_NO_FRACTION;
  mm->num.unit_sys = scene->unit.system;
  /* No time unit supporting frames currently... */
  mm->num.unit_type[0] = ed_marker_move_use_time(mm) ? B_UNIT_TIME : B_UNIT_NONE;

  for (a = 0, marker = static_cast<TimeMarker *>(markers->first); marker; marker = marker->next) {
    if (marker->flag & SELECT) {
      mm->oldframe[a] = marker->frame;
      a++;
    }
  }

  return true;
}

/* free stuff */
static void ed_marker_move_exit(bContext *C, wmOperator *op)
{
  MarkerMove *mm = static_cast<MarkerMove *>(op->customdata);

  /* free data */
  MEM_freeN(mm->oldframe);
  MEM_freeN(op->customdata);
  op->customdata = nullptr;

  /* clear custom header prints */
  ED_area_status_text(CTX_wm_area(C), nullptr);
}

static int ed_marker_move_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
  const bool tweak = RNA_struct_find_property(op->ptr, "tweak") &&
                     RNA_boolean_get(op->ptr, "tweak");

  if (tweak) {
    ARegion *region = CTX_wm_region(C);
    View2D *v2d = &region->v2d;
    ListBase *markers = ED_context_get_markers(C);
    if (!region_position_is_over_marker(v2d, markers, event->xy[0] - region->winrct.xmin)) {
      return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
    }
  }

  if (ed_marker_move_init(C, op)) {
    MarkerMove *mm = static_cast<MarkerMove *>(op->customdata);

    mm->evtx = event->xy[0];
    mm->firstx = event->xy[0];
    mm->event_type = event->type;
    mm->event_val = event->val;

    /* add temp handler */
    WM_event_add_modal_handler(C, op);

    /* Reset frames delta. */
    RNA_int_set(op->ptr, "frames", 0);

    ed_marker_move_update_header(C, op);

    return OPERATOR_RUNNING_MODAL;
  }

  return OPERATOR_CANCELLED;
}

/* NOTE: init has to be called successfully. */
static void ed_marker_move_apply(bContext *C, wmOperator *op)
{
#ifdef DURIAN_CAMERA_SWITCH
  bScreen *screen = CTX_wm_screen(C);
  Scene *scene = CTX_data_scene(C);
  Object *camera = scene->camera;
#endif
  MarkerMove *mm = static_cast<MarkerMove *>(op->customdata);
  TimeMarker *marker;
  int a, ofs;

  ofs = RNA_int_get(op->ptr, "frames");
  for (a = 0, marker = static_cast<TimeMarker *>(mm->markers->first); marker;
       marker = marker->next)
  {
    if (marker->flag & SELECT) {
      marker->frame = mm->oldframe[a] + ofs;
      a++;
    }
  }

  WM_event_add_notifier(C, NC_SCENE | ND_MARKERS, nullptr);
  WM_event_add_notifier(C, NC_ANIMATION | ND_MARKERS, nullptr);

#ifdef DURIAN_CAMERA_SWITCH
  /* so we get view3d redraws */
  BKE_scene_camera_switch_update(scene);

  if (camera != scene->camera) {
    BKE_screen_view3d_scene_sync(screen, scene);
    WM_event_add_notifier(C, NC_SCENE | NA_EDITED, scene);
  }
#endif
}

/* only for modal */
static void ed_marker_move_cancel(bContext *C, wmOperator *op)
{
  RNA_int_set(op->ptr, "frames", 0);
  ed_marker_move_apply(C, op);
  ed_marker_move_exit(C, op);
}

static int ed_marker_move_modal(bContext *C, wmOperator *op, const wmEvent *event)
{
  Scene *scene = CTX_data_scene(C);
  MarkerMove *mm = static_cast<MarkerMove *>(op->customdata);
  View2D *v2d = UI_view2d_fromcontext(C);
  const bool has_numinput = hasNumInput(&mm->num);
  const bool use_time = ed_marker_move_use_time(mm);

  /* Modal numinput active, try to handle numeric inputs first... */
  if (event->val == KM_PRESS && has_numinput && handleNumInput(C, &mm->num, event)) {
    float value = float(RNA_int_get(op->ptr, "frames"));

    applyNumInput(&mm->num, &value);
    if (use_time) {
      value = TIME2FRA(value);
    }

    RNA_int_set(op->ptr, "frames", int(value));
    ed_marker_move_apply(C, op);
    ed_marker_move_update_header(C, op);
  }
  else {
    bool handled = false;
    switch (event->type) {
      case EVT_ESCKEY:
        ed_marker_move_cancel(C, op);
        return OPERATOR_CANCELLED;
      case RIGHTMOUSE:
        /* press = user manually demands transform to be canceled */
        if (event->val == KM_PRESS) {
          ed_marker_move_cancel(C, op);
          return OPERATOR_CANCELLED;
        }
        /* else continue; <--- see if release event should be caught for tweak-end */
        ATTR_FALLTHROUGH;

      case EVT_RETKEY:
      case EVT_PADENTER:
      case LEFTMOUSE:
      case MIDDLEMOUSE:
        if (WM_event_is_modal_drag_exit(event, mm->event_type, mm->event_val)) {
          ed_marker_move_exit(C, op);
          WM_event_add_notifier(C, NC_SCENE | ND_MARKERS, nullptr);
          WM_event_add_notifier(C, NC_ANIMATION | ND_MARKERS, nullptr);
          return OPERATOR_FINISHED;
        }
        break;
      case MOUSEMOVE:
        if (!has_numinput) {
          float dx;

          dx = BLI_rctf_size_x(&v2d->cur) / BLI_rcti_size_x(&v2d->mask);

          if (event->xy[0] != mm->evtx) { /* XXX maybe init for first time */
            float fac;

            mm->evtx = event->xy[0];
            fac = (float(event->xy[0] - mm->firstx) * dx);

            apply_keyb_grid((event->modifier & KM_SHIFT) != 0,
                            (event->modifier & KM_CTRL) != 0,
                            &fac,
                            0.0,
                            FPS,
                            0.1 * FPS,
                            0);

            RNA_int_set(op->ptr, "frames", int(fac));
            ed_marker_move_apply(C, op);
            ed_marker_move_update_header(C, op);
          }
        }
        break;
    }

    if (!handled && event->val == KM_PRESS && handleNumInput(C, &mm->num, event)) {
      float value = float(RNA_int_get(op->ptr, "frames"));

      applyNumInput(&mm->num, &value);
      if (use_time) {
        value = TIME2FRA(value);
      }

      RNA_int_set(op->ptr, "frames", int(value));
      ed_marker_move_apply(C, op);
      ed_marker_move_update_header(C, op);
    }
  }

  return OPERATOR_RUNNING_MODAL;
}

static int ed_marker_move_exec(bContext *C, wmOperator *op)
{
  if (ed_marker_move_init(C, op)) {
    ed_marker_move_apply(C, op);
    ed_marker_move_exit(C, op);
    return OPERATOR_FINISHED;
  }
  return OPERATOR_PASS_THROUGH;
}

static void MARKER_OT_move(wmOperatorType *ot)
{
  /* identifiers */
  ot->name = "Move Time Marker";
  ot->description = "Move selected time marker(s)";
  ot->idname = "MARKER_OT_move";

  /* api callbacks */
  ot->exec = ed_marker_move_exec;
  ot->invoke = ed_marker_move_invoke;
  ot->modal = ed_marker_move_modal;
  ot->poll = ed_markers_poll_selected_no_locked_markers;
  ot->cancel = ed_marker_move_cancel;

  /* flags */
  ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_BLOCKING | OPTYPE_GRAB_CURSOR_X;

  /* rna storage */
  RNA_def_int(ot->srna, "frames", 0, INT_MIN, INT_MAX, "Frames", "", INT_MIN, INT_MAX);
  PropertyRNA *prop = RNA_def_boolean(
      ot->srna, "tweak", false, "Tweak", "Operator has been activated using a click-drag event");
  RNA_def_property_flag(prop, PROP_SKIP_SAVE | PROP_HIDDEN);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Duplicate Markers
 * \{ */

/* operator state vars used:
 *    frames: delta movement
 *
 * functions:
 *
 *     apply()  do the actual duplicate
 *
 * callbacks:
 *
 *     exec()    calls apply, move_exec
 *
 *     invoke() calls apply, move_invoke
 *
 *     modal()    uses move_modal
 */

/* duplicate selected TimeMarkers */
static void ed_marker_duplicate_apply(bContext *C)
{
  ListBase *markers = ED_context_get_markers(C);
  if (markers == nullptr) {
    return;
  }

  /* go through the list of markers, duplicate selected markers and add duplicated copies
   * to the beginning of the list (unselect original markers)
   */
  LISTBASE_FOREACH (TimeMarker *, marker, markers) {
    if (marker->flag & SELECT) {
      /* unselect selected marker */
      marker->flag &= ~SELECT;

      /* create and set up new marker */
      TimeMarker *newmarker = static_cast<TimeMarker *>(
          MEM_callocN(sizeof(TimeMarker), "TimeMarker"));
      newmarker->flag = SELECT;
      newmarker->frame = marker->frame;
      STRNCPY(newmarker->name, marker->name);

#ifdef DURIAN_CAMERA_SWITCH
      newmarker->camera = marker->camera;
#endif

      if (marker->prop != nullptr) {
        newmarker->prop = IDP_CopyProperty(marker->prop);
      }

      /* new marker is added to the beginning of list */
      /* FIXME: bad ordering! */
      BLI_addhead(markers, newmarker);
    }
  }
}

static int ed_marker_duplicate_exec(bContext *C, wmOperator *op)
{
  ed_marker_duplicate_apply(C);
  ed_marker_move_exec(C, op); /* Assumes frame delta set. */

  return OPERATOR_FINISHED;
}

static int ed_marker_duplicate_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
  ed_marker_duplicate_apply(C);
  return ed_marker_move_invoke(C, op, event);
}

static void MARKER_OT_duplicate(wmOperatorType *ot)
{
  /* identifiers */
  ot->name = "Duplicate Time Marker";
  ot->description = "Duplicate selected time marker(s)";
  ot->idname = "MARKER_OT_duplicate";

  /* api callbacks */
  ot->exec = ed_marker_duplicate_exec;
  ot->invoke = ed_marker_duplicate_invoke;
  ot->modal = ed_marker_move_modal;
  ot->poll = ed_markers_poll_selected_no_locked_markers;
  ot->cancel = ed_marker_move_cancel;

  /* flags */
  ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

  /* rna storage */
  RNA_def_int(ot->srna, "frames", 0, INT_MIN, INT_MAX, "Frames", "", INT_MIN, INT_MAX);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Pick Select Markers
 *
 * Select/de-select time-marker at the current frame.
 * \{ */

static void deselect_markers(ListBase *markers)
{
  LISTBASE_FOREACH (TimeMarker *, marker, markers) {
    marker->flag &= ~SELECT;
  }
}

static int select_timeline_marker_frame(ListBase *markers,
                                        int frame,
                                        bool extend,
                                        bool wait_to_deselect_others)
{
  TimeMarker *marker, *marker_cycle_selected = nullptr;
  int ret_val = OPERATOR_FINISHED;

  if (extend) {
    wait_to_deselect_others = false;
  }

  /* support for selection cycling */
  LISTBASE_FOREACH (TimeMarker *, marker, markers) {
    if (marker->frame == frame) {
      if (marker->flag & SELECT) {
        marker_cycle_selected = static_cast<TimeMarker *>(marker->next ? marker->next :
                                                                         markers->first);
        break;
      }
    }
  }

  if (wait_to_deselect_others && marker_cycle_selected) {
    ret_val = OPERATOR_RUNNING_MODAL;
  }
  /* if extend is not set, then deselect markers */
  else {
    if (extend == false) {
      deselect_markers(markers);
    }

    LISTBASE_CIRCULAR_FORWARD_BEGIN (TimeMarker *, markers, marker, marker_cycle_selected) {
      /* this way a not-extend select will always give 1 selected marker */
      if (marker->frame == frame) {
        marker->flag ^= SELECT;
        break;
      }
    }
    LISTBASE_CIRCULAR_FORWARD_END(TimeMarker *, markers, marker, marker_cycle_selected);
  }

  return ret_val;
}

static void select_marker_camera_switch(
    bContext *C, bool camera, bool extend, ListBase *markers, int cfra)
{
  using namespace blender::ed;
#ifdef DURIAN_CAMERA_SWITCH
  if (camera) {
    BLI_assert(CTX_data_mode_enum(C) == CTX_MODE_OBJECT);
    Scene *scene = CTX_data_scene(C);
    ViewLayer *view_layer = CTX_data_view_layer(C);
    Base *base;
    int sel = 0;

    if (!extend) {
      BKE_view_layer_base_deselect_all(scene, view_layer);
    }

    LISTBASE_FOREACH (TimeMarker *, marker, markers) {
      if (marker->frame == cfra) {
        sel = (marker->flag & SELECT);
        break;
      }
    }

    BKE_view_layer_synced_ensure(scene, view_layer);
    LISTBASE_FOREACH (TimeMarker *, marker, markers) {
      if (marker->camera) {
        if (marker->frame == cfra) {
          base = BKE_view_layer_base_find(view_layer, marker->camera);
          if (base) {
            object::base_select(base, object::eObjectSelect_Mode(sel));
            if (sel) {
              object::base_activate(C, base);
            }
          }
        }
      }
    }

    DEG_id_tag_update(&scene->id, ID_RECALC_SELECT);
    WM_event_add_notifier(C, NC_SCENE | ND_OB_SELECT, scene);
  }
#else
  (void)camera;
#endif
}

static int ed_marker_select(
    bContext *C, const int mval[2], bool extend, bool camera, bool wait_to_deselect_others)
{
  ListBase *markers = ED_context_get_markers(C);
  View2D *v2d = UI_view2d_fromcontext(C);
  int ret_val = OPERATOR_FINISHED;

  if (region_position_is_over_marker(v2d, markers, mval[0])) {
    float frame_at_mouse_position = UI_view2d_region_to_view_x(v2d, mval[0]);
    int cfra = ED_markers_find_nearest_marker_time(markers, frame_at_mouse_position);
    ret_val = select_timeline_marker_frame(markers, cfra, extend, wait_to_deselect_others);

    select_marker_camera_switch(C, camera, extend, markers, cfra);
  }
  else {
    deselect_markers(markers);
  }

  WM_event_add_notifier(C, NC_SCENE | ND_MARKERS, nullptr);
  WM_event_add_notifier(C, NC_ANIMATION | ND_MARKERS, nullptr);

  /* allowing tweaks, but needs OPERATOR_FINISHED, otherwise renaming fails, see #25987. */
  return ret_val | OPERATOR_PASS_THROUGH;
}

static int ed_marker_select_exec(bContext *C, wmOperator *op)
{
  const bool extend = RNA_boolean_get(op->ptr, "extend");
  const bool wait_to_deselect_others = RNA_boolean_get(op->ptr, "wait_to_deselect_others");
  bool camera = false;
#ifdef DURIAN_CAMERA_SWITCH
  camera = RNA_boolean_get(op->ptr, "camera");
  if (camera) {
    /* Supporting mode switching from this operator doesn't seem so useful.
     * So only allow setting the active camera in object-mode. */
    if (CTX_data_mode_enum(C) != CTX_MODE_OBJECT) {
      BKE_report(
          op->reports, RPT_WARNING, "Selecting the camera is only supported in object mode");
      camera = false;
    }
  }
#endif
  int mval[2];
  mval[0] = RNA_int_get(op->ptr, "mouse_x");
  mval[1] = RNA_int_get(op->ptr, "mouse_y");

  return ed_marker_select(C, mval, extend, camera, wait_to_deselect_others);
}

static void MARKER_OT_select(wmOperatorType *ot)
{
  PropertyRNA *prop;

  /* identifiers */
  ot->name = "Select Time Marker";
  ot->description = "Select time marker(s)";
  ot->idname = "MARKER_OT_select";

  /* api callbacks */
  ot->poll = ed_markers_poll_markers_exist;
  ot->exec = ed_marker_select_exec;
  ot->invoke = WM_generic_select_invoke;
  ot->modal = WM_generic_select_modal;

  /* flags */
  ot->flag = OPTYPE_UNDO;

  WM_operator_properties_generic_select(ot);
  prop = RNA_def_boolean(ot->srna, "extend", false, "Extend", "Extend the selection");
  RNA_def_property_flag(prop, PROP_SKIP_SAVE);
#ifdef DURIAN_CAMERA_SWITCH
  prop = RNA_def_boolean(ot->srna, "camera", false, "Camera", "Select the camera");
  RNA_def_property_flag(prop, PROP_SKIP_SAVE);
#endif
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Box Select Markers
 * \{ */

/* operator state vars used: (added by default WM callbacks)
 * xmin, ymin
 * xmax, ymax
 *
 * customdata: the wmGesture pointer, with sub-window.
 *
 * callbacks:
 *
 *  exec()  has to be filled in by user
 *
 *  invoke() default WM function
 *          adds modal handler
 *
 *  modal() default WM function
 *          accept modal events while doing it, calls exec(), handles ESC and border drawing
 *
 *  poll()  has to be filled in by user for context
 */

static int ed_marker_box_select_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
  ARegion *region = CTX_wm_region(C);
  View2D *v2d = &region->v2d;

  ListBase *markers = ED_context_get_markers(C);
  bool over_marker = region_position_is_over_marker(
      v2d, markers, event->xy[0] - region->winrct.xmin);

  bool tweak = RNA_boolean_get(op->ptr, "tweak");
  if (tweak && over_marker) {
    return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
  }

  return WM_gesture_box_invoke(C, op, event);
}

static int ed_marker_box_select_exec(bContext *C, wmOperator *op)
{
  View2D *v2d = UI_view2d_fromcontext(C);
  ListBase *markers = ED_context_get_markers(C);
  rctf rect;

  WM_operator_properties_border_to_rctf(op, &rect);
  UI_view2d_region_to_view_rctf(v2d, &rect, &rect);

  if (markers == nullptr) {
    return 0;
  }

  const eSelectOp sel_op = eSelectOp(RNA_enum_get(op->ptr, "mode"));
  const bool select = (sel_op != SEL_OP_SUB);
  if (SEL_OP_USE_PRE_DESELECT(sel_op)) {
    ED_markers_deselect_all(markers, SEL_DESELECT);
  }

  LISTBASE_FOREACH (TimeMarker *, marker, markers) {
    if (BLI_rctf_isect_x(&rect, marker->frame)) {
      SET_FLAG_FROM_TEST(marker->flag, select, SELECT);
    }
  }

  WM_event_add_notifier(C, NC_SCENE | ND_MARKERS, nullptr);
  WM_event_add_notifier(C, NC_ANIMATION | ND_MARKERS, nullptr);

  return 1;
}

static void MARKER_OT_select_box(wmOperatorType *ot)
{
  /* identifiers */
  ot->name = "Marker Box Select";
  ot->description = "Select all time markers using box selection";
  ot->idname = "MARKER_OT_select_box";

  /* api callbacks */
  ot->exec = ed_marker_box_select_exec;
  ot->invoke = ed_marker_box_select_invoke;
  ot->modal = WM_gesture_box_modal;
  ot->cancel = WM_gesture_box_cancel;

  ot->poll = ed_markers_poll_markers_exist;

  /* flags */
  ot->flag = OPTYPE_UNDO;

  /* properties */
  WM_operator_properties_gesture_box(ot);
  WM_operator_properties_select_operation_simple(ot);

  PropertyRNA *prop = RNA_def_boolean(
      ot->srna, "tweak", false, "Tweak", "Operator has been activated using a click-drag event");
  RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name (de)select all
 * \{ */

static int ed_marker_select_all_exec(bContext *C, wmOperator *op)
{
  ListBase *markers = ED_context_get_markers(C);
  if (markers == nullptr) {
    return OPERATOR_CANCELLED;
  }

  int action = RNA_enum_get(op->ptr, "action");
  ED_markers_deselect_all(markers, action);

  WM_event_add_notifier(C, NC_SCENE | ND_MARKERS, nullptr);
  WM_event_add_notifier(C, NC_ANIMATION | ND_MARKERS, nullptr);

  return OPERATOR_FINISHED;
}

static void MARKER_OT_select_all(wmOperatorType *ot)
{
  /* identifiers */
  ot->name = "(De)select all Markers";
  ot->description = "Change selection of all time markers";
  ot->idname = "MARKER_OT_select_all";

  /* api callbacks */
  ot->exec = ed_marker_select_all_exec;
  ot->poll = ed_markers_poll_markers_exist;

  /* flags */
  ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

  /* rna */
  WM_operator_properties_select_all(ot);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Select Left/Right of Frame
 * \{ */

enum eMarkers_LeftRightSelect_Mode {
  MARKERS_LRSEL_LEFT = 0,
  MARKERS_LRSEL_RIGHT,
};

static const EnumPropertyItem prop_markers_select_leftright_modes[] = {
    {MARKERS_LRSEL_LEFT, "LEFT", 0, "Before Current Frame", ""},
    {MARKERS_LRSEL_RIGHT, "RIGHT", 0, "After Current Frame", ""},
    {0, nullptr, 0, nullptr, nullptr},
};

static void ED_markers_select_leftright(bAnimContext *ac,
                                        const eMarkers_LeftRightSelect_Mode mode,
                                        const bool extend)
{
  ListBase *markers = ac->markers;
  Scene *scene = ac->scene;

  if (markers == nullptr) {
    return;
  }

  if (!extend) {
    deselect_markers(markers);
  }

  LISTBASE_FOREACH (TimeMarker *, marker, markers) {
    if ((mode == MARKERS_LRSEL_LEFT && marker->frame <= scene->r.cfra) ||
        (mode == MARKERS_LRSEL_RIGHT && marker->frame >= scene->r.cfra))
    {
      marker->flag |= SELECT;
    }
  }
}

static int ed_marker_select_leftright_exec(bContext *C, wmOperator *op)
{
  const eMarkers_LeftRightSelect_Mode mode = eMarkers_LeftRightSelect_Mode(
      RNA_enum_get(op->ptr, "mode"));
  const bool extend = RNA_boolean_get(op->ptr, "extend");

  bAnimContext ac;
  if (ANIM_animdata_get_context(C, &ac) == 0) {
    return OPERATOR_CANCELLED;
  }

  ED_markers_select_leftright(&ac, mode, extend);

  WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_SELECTED, nullptr);

  return OPERATOR_FINISHED;
}

static void MARKER_OT_select_leftright(wmOperatorType *ot)
{
  /* identifiers */
  ot->name = "Select Markers Before/After Current Frame";
  ot->description = "Select markers on and left/right of the current frame";
  ot->idname = "MARKER_OT_select_leftright";

  /* api callbacks */
  ot->exec = ed_marker_select_leftright_exec;
  ot->poll = ed_markers_poll_markers_exist;

  /* flags */
  ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

  /* rna storage */
  RNA_def_enum(
      ot->srna, "mode", prop_markers_select_leftright_modes, MARKERS_LRSEL_LEFT, "Mode", "");
  RNA_def_boolean(ot->srna, "extend", false, "Extend Select", "");
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Remove Marker
 *
 * Remove selected time-markers.
 * \{ */

static int ed_marker_delete_exec(bContext *C, wmOperator * /*op*/)

{
  ListBase *markers = ED_context_get_markers(C);
  TimeMarker *marker, *nmarker;
  bool changed = false;

  if (markers == nullptr) {
    return OPERATOR_CANCELLED;
  }

  for (marker = static_cast<TimeMarker *>(markers->first); marker; marker = nmarker) {
    nmarker = marker->next;
    if (marker->flag & SELECT) {
      if (marker->prop != nullptr) {
        IDP_FreePropertyContent(marker->prop);
        MEM_freeN(marker->prop);
      }
      BLI_freelinkN(markers, marker);
      changed = true;
    }
  }

  if (changed) {
    WM_event_add_notifier(C, NC_SCENE | ND_MARKERS, nullptr);
    WM_event_add_notifier(C, NC_ANIMATION | ND_MARKERS, nullptr);
  }

  return OPERATOR_FINISHED;
}

static int ed_marker_delete_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/)
{
  if (RNA_boolean_get(op->ptr, "confirm")) {
    return WM_operator_confirm_ex(C,
                                  op,
                                  IFACE_("Delete selected markers?"),
                                  nullptr,
                                  IFACE_("Delete"),
                                  ALERT_ICON_NONE,
                                  false);
  }
  return ed_marker_delete_exec(C, op);
}

static void MARKER_OT_delete(wmOperatorType *ot)
{
  /* identifiers */
  ot->name = "Delete Markers";
  ot->description = "Delete selected time marker(s)";
  ot->idname = "MARKER_OT_delete";

  /* api callbacks */
  ot->invoke = ed_marker_delete_invoke;
  ot->exec = ed_marker_delete_exec;
  ot->poll = ed_markers_poll_selected_no_locked_markers;

  /* flags */
  ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  WM_operator_properties_confirm_or_exec(ot);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Rename Marker
 *
 * Rename first selected time-marker.
 * \{ */

static int ed_marker_rename_exec(bContext *C, wmOperator *op)
{
  TimeMarker *marker = ED_markers_get_first_selected(ED_context_get_markers(C));

  if (marker) {
    RNA_string_get(op->ptr, "name", marker->name);

    WM_event_add_notifier(C, NC_SCENE | ND_MARKERS, nullptr);
    WM_event_add_notifier(C, NC_ANIMATION | ND_MARKERS, nullptr);

    return OPERATOR_FINISHED;
  }

  return OPERATOR_CANCELLED;
}

static int ed_marker_rename_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
  /* must initialize the marker name first if there is a marker selected */
  TimeMarker *marker = ED_markers_get_first_selected(ED_context_get_markers(C));
  if (marker) {
    RNA_string_set(op->ptr, "name", marker->name);
  }

  return WM_operator_props_popup_confirm_ex(
      C, op, event, IFACE_("Rename Selected Time Marker"), IFACE_("Rename"));
}

static void MARKER_OT_rename(wmOperatorType *ot)
{
  /* identifiers */
  ot->name = "Rename Marker";
  ot->description = "Rename first selected time marker";
  ot->idname = "MARKER_OT_rename";

  /* api callbacks */
  ot->invoke = ed_marker_rename_invoke;
  ot->exec = ed_marker_rename_exec;
  ot->poll = ed_markers_poll_selected_no_locked_markers;

  /* flags */
  ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

  /* properties */
  ot->prop = RNA_def_string(
      ot->srna, "name", "RenamedMarker", sizeof(TimeMarker::name), "Name", "New name for marker");
#if 0
  RNA_def_boolean(ot->srna,
                  "ensure_unique",
                  0,
                  "Ensure Unique",
                  "Ensure that new name is unique within collection of markers");
#endif
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Make Links to Scene
 * \{ */

static int ed_marker_make_links_scene_exec(bContext *C, wmOperator *op)
{
  Main *bmain = CTX_data_main(C);
  ListBase *markers = ED_context_get_markers(C);
  Scene *scene_to = static_cast<Scene *>(
      BLI_findlink(&bmain->scenes, RNA_enum_get(op->ptr, "scene")));
  TimeMarker *marker_new;

  if (scene_to == nullptr) {
    BKE_report(op->reports, RPT_ERROR, "Scene not found");
    return OPERATOR_CANCELLED;
  }

  if (scene_to == CTX_data_scene(C)) {
    BKE_report(op->reports, RPT_ERROR, "Cannot re-link markers into the same scene");
    return OPERATOR_CANCELLED;
  }

  if (scene_to->toolsettings->lock_markers) {
    BKE_report(op->reports, RPT_ERROR, "Target scene has locked markers");
    return OPERATOR_CANCELLED;
  }

  /* copy markers */
  LISTBASE_FOREACH (TimeMarker *, marker, markers) {
    if (marker->flag & SELECT) {
      marker_new = static_cast<TimeMarker *>(MEM_dupallocN(marker));
      marker_new->prev = marker_new->next = nullptr;

      BLI_addtail(&scene_to->markers, marker_new);
    }
  }

  return OPERATOR_FINISHED;
}

static void MARKER_OT_make_links_scene(wmOperatorType *ot)
{
  PropertyRNA *prop;

  /* identifiers */
  ot->name = "Copy Markers to Scene";
  ot->description = "Copy selected markers to another scene";
  ot->idname = "MARKER_OT_make_links_scene";

  /* api callbacks */
  ot->exec = ed_marker_make_links_scene_exec;
  ot->invoke = WM_menu_invoke;
  ot->poll = ed_markers_poll_selected_markers;

  /* flags */
  ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

  /* properties */
  prop = RNA_def_enum(ot->srna, "scene", rna_enum_dummy_NULL_items, 0, "Scene", "");
  RNA_def_enum_funcs(prop, RNA_scene_itemf);
  RNA_def_property_flag(prop, PROP_ENUM_NO_TRANSLATE);
  ot->prop = prop;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Camera Bind Marker
 * \{ */

#ifdef DURIAN_CAMERA_SWITCH

static int ed_marker_camera_bind_exec(bContext *C, wmOperator *op)
{
  bScreen *screen = CTX_wm_screen(C);
  Scene *scene = CTX_data_scene(C);
  Object *ob = CTX_data_active_object(C);
  ListBase *markers = ED_context_get_markers(C);
  TimeMarker *marker;

  /* Don't do anything if we don't have a camera selected */
  if (ob == nullptr) {
    BKE_report(op->reports, RPT_ERROR, "Select a camera to bind to a marker on this frame");
    return OPERATOR_CANCELLED;
  }

  /* add new marker, unless we already have one on this frame, in which case, replace it */
  if (markers == nullptr) {
    return OPERATOR_CANCELLED;
  }

  marker = ED_markers_find_nearest_marker(markers, scene->r.cfra);
  if ((marker == nullptr) || (marker->frame != scene->r.cfra)) {
    marker = static_cast<TimeMarker *>(MEM_callocN(sizeof(TimeMarker), "Camera TimeMarker"));
    /* This marker's name is only displayed in the viewport statistics, animation editors use the
     * camera's name when bound to a marker. */
    SNPRINTF(marker->name, "F_%02d", scene->r.cfra);
    marker->flag = SELECT;
    marker->frame = scene->r.cfra;
    BLI_addtail(markers, marker);

    /* deselect all others, so that the user can then move it without problems */
    LISTBASE_FOREACH (TimeMarker *, m, markers) {
      if (m != marker) {
        m->flag &= ~SELECT;
      }
    }
  }

  /* bind to the nominated camera (as set in operator props) */
  marker->camera = ob;

  /* camera may have changes */
  BKE_scene_camera_switch_update(scene);
  BKE_screen_view3d_scene_sync(screen, scene);
  DEG_relations_tag_update(CTX_data_main(C));

  WM_event_add_notifier(C, NC_SCENE | ND_MARKERS, nullptr);
  WM_event_add_notifier(C, NC_ANIMATION | ND_MARKERS, nullptr);
  WM_event_add_notifier(C, NC_SCENE | NA_EDITED, scene); /* so we get view3d redraws */

  return OPERATOR_FINISHED;
}

static void MARKER_OT_camera_bind(wmOperatorType *ot)
{
  /* identifiers */
  ot->name = "Bind Camera to Markers";
  ot->description = "Bind the selected camera to a marker on the current frame";
  ot->idname = "MARKER_OT_camera_bind";

  /* api callbacks */
  ot->exec = ed_marker_camera_bind_exec;
  ot->poll = ED_operator_markers_region_active;

  /* flags */
  ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}

#endif

/** \} */

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

void ED_operatortypes_marker()
{
  WM_operatortype_append(MARKER_OT_add);
  WM_operatortype_append(MARKER_OT_move);
  WM_operatortype_append(MARKER_OT_duplicate);
  WM_operatortype_append(MARKER_OT_select);
  WM_operatortype_append(MARKER_OT_select_box);
  WM_operatortype_append(MARKER_OT_select_all);
  WM_operatortype_append(MARKER_OT_select_leftright);
  WM_operatortype_append(MARKER_OT_delete);
  WM_operatortype_append(MARKER_OT_rename);
  WM_operatortype_append(MARKER_OT_make_links_scene);
#ifdef DURIAN_CAMERA_SWITCH
  WM_operatortype_append(MARKER_OT_camera_bind);
#endif
}

void ED_keymap_marker(wmKeyConfig *keyconf)
{
  WM_keymap_ensure(keyconf, "Markers", SPACE_EMPTY, RGN_TYPE_WINDOW);
}

/** \} */
