/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later */

/** \file
 * \ingroup spseq
 */

#include <cmath>
#include <cstdlib>
#include <cstring>

#include "MEM_guardedalloc.h"

#include "BLI_ghash.h"
#include "BLI_listbase.h"
#include "BLI_math_geom.h"
#include "BLI_math_vector.h"
#include "BLI_math_vector_types.hh"
#include "BLI_string.h"
#include "BLI_utildefines.h"

#include "DNA_scene_types.h"
#include "DNA_space_types.h"

#include "BKE_context.hh"
#include "BKE_report.hh"

#include "BLT_translation.hh"

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

#include "RNA_define.hh"

#include "SEQ_channels.hh"
#include "SEQ_connect.hh"
#include "SEQ_effects.hh"
#include "SEQ_iterator.hh"
#include "SEQ_relations.hh"
#include "SEQ_retiming.hh"
#include "SEQ_select.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_time.hh"
#include "SEQ_transform.hh"

/* For menu, popup, icons, etc. */

#include "ED_outliner.hh"
#include "ED_screen.hh"
#include "ED_select_utils.hh"
#include "ED_sequencer.hh"

#include "UI_view2d.hh"

/* Own include. */
#include "sequencer_intern.hh"

namespace blender::ed::vse {

/* -------------------------------------------------------------------- */
/** \name Selection Utilities
 * \{ */

class MouseCoords {
 public:
  blender::int2 region;
  blender::float2 view;

  MouseCoords(const View2D *v2d, int x, int y)
  {
    region[0] = x;
    region[1] = y;
    UI_view2d_region_to_view(v2d, x, y, &view[0], &view[1]);
  }
};

bool deselect_all_strips(const Scene *scene)
{
  Editing *ed = seq::editing_get(scene);
  bool changed = false;

  if (ed == nullptr) {
    return changed;
  }

  VectorSet<Strip *> strips = seq::query_all_strips(seq::active_seqbase_get(ed));
  for (Strip *strip : strips) {
    if (strip->flag & STRIP_ALLSEL) {
      strip->flag &= ~STRIP_ALLSEL;
      changed = true;
    }
  }
  return changed;
}

Strip *strip_under_mouse_get(const Scene *scene, const View2D *v2d, const int mval[2])
{
  float mouse_co[2];
  UI_view2d_region_to_view(v2d, mval[0], mval[1], &mouse_co[0], &mouse_co[1]);

  blender::Vector<Strip *> visible = sequencer_visible_strips_get(scene, v2d);
  int mouse_channel = int(mouse_co[1]);
  for (Strip *strip : visible) {
    if (strip->channel != mouse_channel) {
      continue;
    }
    rctf body;
    strip_rectf(scene, strip, &body);
    if (BLI_rctf_isect_pt_v(&body, mouse_co)) {
      return strip;
    }
  }

  return nullptr;
}

blender::VectorSet<Strip *> all_strips_from_context(bContext *C)
{
  Scene *scene = CTX_data_scene(C);
  Editing *ed = seq::editing_get(scene);
  ListBase *seqbase = seq::active_seqbase_get(ed);
  ListBase *channels = seq::channels_displayed_get(ed);

  const bool is_preview = sequencer_view_has_preview_poll(C);
  if (is_preview) {
    return seq::query_rendered_strips(scene, channels, seqbase, scene->r.cfra, 0);
  }

  return seq::query_all_strips(seqbase);
}

blender::VectorSet<Strip *> selected_strips_from_context(bContext *C)
{
  const Scene *scene = CTX_data_scene(C);
  Editing *ed = seq::editing_get(scene);
  ListBase *seqbase = seq::active_seqbase_get(ed);
  ListBase *channels = seq::channels_displayed_get(ed);

  const bool is_preview = sequencer_view_has_preview_poll(C);

  if (is_preview) {
    blender::VectorSet strips = seq::query_rendered_strips(
        scene, channels, seqbase, scene->r.cfra, 0);
    strips.remove_if([&](Strip *strip) { return (strip->flag & SELECT) == 0; });
    return strips;
  }

  return seq::query_selected_strips(seqbase);
}

static void select_surrounding_handles(Scene *scene, Strip *test) /* XXX BRING BACK */
{
  Strip *neighbor;

  neighbor = find_neighboring_strip(scene, test, seq::SIDE_LEFT, -1);
  if (neighbor) {
    /* Only select neighbor handle if matching handle from test strip is also selected,
     * or if neighbor was not selected at all up till now.
     * Otherwise, we get odd mismatch when shift-alt-rmb selecting neighbor strips... */
    if (!(neighbor->flag & SELECT) || (test->flag & SEQ_LEFTSEL)) {
      neighbor->flag |= SEQ_RIGHTSEL;
    }
    neighbor->flag |= SELECT;
    recurs_sel_strip(neighbor);
  }
  neighbor = find_neighboring_strip(scene, test, seq::SIDE_RIGHT, -1);
  if (neighbor) {
    if (!(neighbor->flag & SELECT) || (test->flag & SEQ_RIGHTSEL)) { /* See comment above. */
      neighbor->flag |= SEQ_LEFTSEL;
    }
    neighbor->flag |= SELECT;
    recurs_sel_strip(neighbor);
  }
}

/* Used for mouse selection in #SEQUENCER_OT_select. */
static void select_active_side(
    const Scene *scene, ListBase *seqbase, int sel_side, int channel, int frame)
{

  LISTBASE_FOREACH (Strip *, strip, seqbase) {
    if (channel == strip->channel) {
      switch (sel_side) {
        case seq::SIDE_LEFT:
          if (frame > seq::time_left_handle_frame_get(scene, strip)) {
            strip->flag &= ~(SEQ_RIGHTSEL | SEQ_LEFTSEL);
            strip->flag |= SELECT;
          }
          break;
        case seq::SIDE_RIGHT:
          if (frame < seq::time_left_handle_frame_get(scene, strip)) {
            strip->flag &= ~(SEQ_RIGHTSEL | SEQ_LEFTSEL);
            strip->flag |= SELECT;
          }
          break;
        case seq::SIDE_BOTH:
          strip->flag &= ~(SEQ_RIGHTSEL | SEQ_LEFTSEL);
          strip->flag |= SELECT;
          break;
      }
    }
  }
}

/* Used for mouse selection in #SEQUENCER_OT_select_side. */
static void select_active_side_range(const Scene *scene,
                                     ListBase *seqbase,
                                     const int sel_side,
                                     const int frame_ranges[seq::MAX_CHANNELS],
                                     const int frame_ignore)
{
  LISTBASE_FOREACH (Strip *, strip, seqbase) {
    if (strip->channel < seq::MAX_CHANNELS) {
      const int frame = frame_ranges[strip->channel];
      if (frame == frame_ignore) {
        continue;
      }
      switch (sel_side) {
        case seq::SIDE_LEFT:
          if (frame > seq::time_left_handle_frame_get(scene, strip)) {
            strip->flag &= ~(SEQ_RIGHTSEL | SEQ_LEFTSEL);
            strip->flag |= SELECT;
          }
          break;
        case seq::SIDE_RIGHT:
          if (frame < seq::time_left_handle_frame_get(scene, strip)) {
            strip->flag &= ~(SEQ_RIGHTSEL | SEQ_LEFTSEL);
            strip->flag |= SELECT;
          }
          break;
        case seq::SIDE_BOTH:
          strip->flag &= ~(SEQ_RIGHTSEL | SEQ_LEFTSEL);
          strip->flag |= SELECT;
          break;
      }
    }
  }
}

/* Used alongside `select_linked_time` helper function in #SEQUENCER_OT_select. */
static void select_linked_time_strip(const Scene *scene,
                                     const Strip *strip_source,
                                     const eStripHandle handle_clicked)
{
  ListBase *seqbase = seq::active_seqbase_get(scene->ed);
  int source_left = seq::time_left_handle_frame_get(scene, strip_source);
  int source_right = seq::time_right_handle_frame_get(scene, strip_source);

  LISTBASE_FOREACH (Strip *, strip_dest, seqbase) {
    if (strip_source->channel != strip_dest->channel) {
      const bool left_match = (seq::time_left_handle_frame_get(scene, strip_dest) == source_left);
      const bool right_match = (seq::time_right_handle_frame_get(scene, strip_dest) ==
                                source_right);

      if (left_match && right_match) {
        /* Direct match, copy all selection settings. */
        strip_dest->flag &= ~STRIP_ALLSEL;
        strip_dest->flag |= strip_source->flag & (STRIP_ALLSEL);
        recurs_sel_strip(strip_dest);
      }
      else if (left_match && handle_clicked == STRIP_HANDLE_LEFT) {
        strip_dest->flag &= ~(SELECT | SEQ_LEFTSEL);
        strip_dest->flag |= strip_source->flag & (SELECT | SEQ_LEFTSEL);
        recurs_sel_strip(strip_dest);
      }
      else if (right_match && handle_clicked == STRIP_HANDLE_RIGHT) {
        strip_dest->flag &= ~(SELECT | SEQ_RIGHTSEL);
        strip_dest->flag |= strip_source->flag & (SELECT | SEQ_RIGHTSEL);
        recurs_sel_strip(strip_dest);
      }
    }
  }
}

#if 0 /* BRING BACK */
void select_surround_from_last(Scene *scene)
{
  Strip *strip = get_last_seq(scene);

  if (strip == nullptr) {
    return;
  }

  select_surrounding_handles(scene, strip);
}
#endif

void select_strip_single(Scene *scene, Strip *strip, bool deselect_all)
{
  Editing *ed = seq::editing_get(scene);

  if (deselect_all) {
    deselect_all_strips(scene);
  }

  seq::select_active_set(scene, strip);

  if (ELEM(strip->type, STRIP_TYPE_IMAGE, STRIP_TYPE_MOVIE)) {
    if (strip->data) {
      BLI_strncpy(ed->act_imagedir, strip->data->dirpath, FILE_MAXDIR);
    }
  }
  else if (strip->type == STRIP_TYPE_SOUND_RAM) {
    if (strip->data) {
      BLI_strncpy(ed->act_sounddir, strip->data->dirpath, FILE_MAXDIR);
    }
  }
  strip->flag |= SELECT;
  recurs_sel_strip(strip);
}

void strip_rectf(const Scene *scene, const Strip *strip, rctf *r_rect)
{
  r_rect->xmin = seq::time_left_handle_frame_get(scene, strip);
  r_rect->xmax = seq::time_right_handle_frame_get(scene, strip);
  r_rect->ymin = strip->channel + STRIP_OFSBOTTOM;
  r_rect->ymax = strip->channel + STRIP_OFSTOP;
}

Strip *find_neighboring_strip(const Scene *scene, const Strip *test, const int lr, int sel)
{
  /* sel: 0==unselected, 1==selected, -1==don't care. */
  Editing *ed = seq::editing_get(scene);

  if (ed == nullptr) {
    return nullptr;
  }

  if (sel > 0) {
    sel = SELECT;
  }
  LISTBASE_FOREACH (Strip *, strip, ed->seqbasep) {
    if ((strip != test) && (test->channel == strip->channel) &&
        ((sel == -1) || (sel && (strip->flag & SELECT)) ||
         (sel == 0 && (strip->flag & SELECT) == 0)))
    {
      switch (lr) {
        case seq::SIDE_LEFT:
          if (seq::time_left_handle_frame_get(scene, test) ==
              seq::time_right_handle_frame_get(scene, strip))
          {
            return strip;
          }
          break;
        case seq::SIDE_RIGHT:
          if (seq::time_right_handle_frame_get(scene, test) ==
              seq::time_left_handle_frame_get(scene, strip))
          {
            return strip;
          }
          break;
      }
    }
  }
  return nullptr;
}

#if 0
static void select_neighbor_from_last(Scene *scene, int lr)
{
  Strip *strip = seq::SEQ_select_active_get(scene);
  Strip *neighbor;
  bool changed = false;
  if (strip) {
    neighbor = find_neighboring_strip(scene, strip, lr, -1);
    if (neighbor) {
      switch (lr) {
        case seq::SIDE_LEFT:
          neighbor->flag |= SELECT;
          recurs_sel_strip(neighbor);
          neighbor->flag |= SEQ_RIGHTSEL;
          strip->flag |= SEQ_LEFTSEL;
          break;
        case seq::SIDE_RIGHT:
          neighbor->flag |= SELECT;
          recurs_sel_strip(neighbor);
          neighbor->flag |= SEQ_LEFTSEL;
          strip->flag |= SEQ_RIGHTSEL;
          break;
      }
      strip->flag |= SELECT;
      changed = true;
    }
  }
  if (changed) {
    /* Pass. */
  }
}
#endif

void recurs_sel_strip(Strip *strip_meta)
{
  Strip *strip;
  strip = static_cast<Strip *>(strip_meta->seqbase.first);

  while (strip) {

    if (strip_meta->flag & (SEQ_LEFTSEL + SEQ_RIGHTSEL)) {
      strip->flag &= ~STRIP_ALLSEL;
    }
    else if (strip_meta->flag & SELECT) {
      strip->flag |= SELECT;
    }
    else {
      strip->flag &= ~STRIP_ALLSEL;
    }

    if (strip->seqbase.first) {
      recurs_sel_strip(strip);
    }

    strip = static_cast<Strip *>(strip->next);
  }
}

bool strip_point_image_isect(const Scene *scene, const Strip *strip, float point_view[2])
{
  const blender::Array<blender::float2> strip_image_quad = seq::image_transform_final_quad_get(
      scene, strip);
  return isect_point_quad_v2(point_view,
                             strip_image_quad[0],
                             strip_image_quad[1],
                             strip_image_quad[2],
                             strip_image_quad[3]);
}

void sequencer_select_do_updates(const bContext *C, Scene *scene)
{
  ED_outliner_select_sync_from_sequence_tag(C);
  WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER | NA_SELECTED, scene);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name (De)select All Operator
 * \{ */

static wmOperatorStatus sequencer_de_select_all_exec(bContext *C, wmOperator *op)
{
  int action = RNA_enum_get(op->ptr, "action");
  Scene *scene = CTX_data_scene(C);

  if (sequencer_view_has_preview_poll(C) && !sequencer_view_preview_only_poll(C)) {
    return OPERATOR_CANCELLED;
  }

  if (sequencer_retiming_mode_is_active(C) && retiming_keys_can_be_displayed(CTX_wm_space_seq(C)))
  {
    return sequencer_retiming_select_all_exec(C, op);
  }

  blender::VectorSet strips = all_strips_from_context(C);

  if (action == SEL_TOGGLE) {
    action = SEL_SELECT;
    for (Strip *strip : strips) {
      if (strip->flag & STRIP_ALLSEL) {
        action = SEL_DESELECT;
        break;
      }
    }
  }
  if (ELEM(action, SEL_INVERT, SEL_SELECT)) {
    if (action == SEL_INVERT) {
      for (Strip *strip : strips) {
        if (strip->flag & STRIP_ALLSEL) {
          strips.remove(strip);
        }
      }
    }
    deselect_all_strips(scene);
  }
  for (Strip *strip : strips) {
    switch (action) {
      case SEL_SELECT:
        strip->flag |= SELECT;
        break;
      case SEL_DESELECT:
        strip->flag &= ~STRIP_ALLSEL;
        break;
      case SEL_INVERT:
        strip->flag |= SELECT;
        break;
    }
  }
  ED_outliner_select_sync_from_sequence_tag(C);
  WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER | NA_SELECTED, scene);

  return OPERATOR_FINISHED;
}

void SEQUENCER_OT_select_all(wmOperatorType *ot)
{
  /* Identifiers. */
  ot->name = "(De)select All";
  ot->idname = "SEQUENCER_OT_select_all";
  ot->description = "Select or deselect all strips";

  /* API callbacks. */
  ot->exec = sequencer_de_select_all_exec;
  ot->poll = sequencer_edit_poll;

  /* Flags. */
  ot->flag = OPTYPE_UNDO;

  WM_operator_properties_select_all(ot);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Select Inverse Operator
 * \{ */

static wmOperatorStatus sequencer_select_inverse_exec(bContext *C, wmOperator * /*op*/)
{
  Scene *scene = CTX_data_scene(C);

  if (sequencer_view_has_preview_poll(C) && !sequencer_view_preview_only_poll(C)) {
    return OPERATOR_CANCELLED;
  }

  blender::VectorSet strips = all_strips_from_context(C);

  for (Strip *strip : strips) {
    if (strip->flag & SELECT) {
      strip->flag &= ~STRIP_ALLSEL;
    }
    else {
      strip->flag &= ~(SEQ_LEFTSEL + SEQ_RIGHTSEL);
      strip->flag |= SELECT;
    }
  }

  ED_outliner_select_sync_from_sequence_tag(C);
  WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER | NA_SELECTED, scene);

  return OPERATOR_FINISHED;
}

void SEQUENCER_OT_select_inverse(wmOperatorType *ot)
{
  /* Identifiers. */
  ot->name = "Select Inverse";
  ot->idname = "SEQUENCER_OT_select_inverse";
  ot->description = "Select unselected strips";

  /* API callbacks. */
  ot->exec = sequencer_select_inverse_exec;
  ot->poll = sequencer_edit_poll;

  /* Flags. */
  ot->flag = OPTYPE_UNDO;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Select Operator
 * \{ */

static void sequencer_select_set_active(Scene *scene, Strip *strip)
{
  Editing *ed = seq::editing_get(scene);

  seq::select_active_set(scene, strip);

  if (ELEM(strip->type, STRIP_TYPE_IMAGE, STRIP_TYPE_MOVIE)) {
    if (strip->data) {
      BLI_strncpy(ed->act_imagedir, strip->data->dirpath, FILE_MAXDIR);
    }
  }
  else if (strip->type == STRIP_TYPE_SOUND_RAM) {
    if (strip->data) {
      BLI_strncpy(ed->act_sounddir, strip->data->dirpath, FILE_MAXDIR);
    }
  }
  recurs_sel_strip(strip);
}

static void sequencer_select_side_of_frame(const bContext *C,
                                           const View2D *v2d,
                                           const int mval[2],
                                           Scene *scene)
{
  Editing *ed = seq::editing_get(scene);

  const float x = UI_view2d_region_to_view_x(v2d, mval[0]);
  LISTBASE_FOREACH (Strip *, strip_iter, seq::active_seqbase_get(ed)) {
    if (((x < scene->r.cfra) &&
         (seq::time_right_handle_frame_get(scene, strip_iter) <= scene->r.cfra)) ||
        ((x >= scene->r.cfra) &&
         (seq::time_left_handle_frame_get(scene, strip_iter) >= scene->r.cfra)))
    {
      /* Select left or right. */
      strip_iter->flag |= SELECT;
      recurs_sel_strip(strip_iter);
    }
  }

  {
    SpaceSeq *sseq = CTX_wm_space_seq(C);
    if (sseq && sseq->flag & SEQ_MARKER_TRANS) {

      LISTBASE_FOREACH (TimeMarker *, tmarker, &scene->markers) {
        if (((x < scene->r.cfra) && (tmarker->frame <= scene->r.cfra)) ||
            ((x >= scene->r.cfra) && (tmarker->frame >= scene->r.cfra)))
        {
          tmarker->flag |= SELECT;
        }
        else {
          tmarker->flag &= ~SELECT;
        }
      }
    }
  }
}

static void sequencer_select_linked_handle(const bContext *C,
                                           Strip *strip,
                                           const eStripHandle handle_clicked)
{
  Scene *scene = CTX_data_scene(C);
  Editing *ed = seq::editing_get(scene);
  if (!ELEM(handle_clicked, STRIP_HANDLE_LEFT, STRIP_HANDLE_RIGHT)) {
    /* First click selects the strip and its adjacent handles (if valid).
     * Second click selects the strip,
     * both of its handles and its adjacent handles (if valid). */
    const bool is_striponly_selected = ((strip->flag & STRIP_ALLSEL) == SELECT);
    strip->flag &= ~STRIP_ALLSEL;
    strip->flag |= is_striponly_selected ? STRIP_ALLSEL : SELECT;
    select_surrounding_handles(scene, strip);
  }
  else {
    /* Always select the strip under the cursor. */
    strip->flag |= SELECT;

    /* First click selects adjacent handles on that side.
     * Second click selects all strips in that direction.
     * If there are no adjacent strips, it just selects all in that direction.
     */
    const int sel_side = (handle_clicked == STRIP_HANDLE_LEFT) ? seq::SIDE_LEFT : seq::SIDE_RIGHT;

    Strip *neighbor = find_neighboring_strip(scene, strip, sel_side, -1);
    if (neighbor) {
      switch (sel_side) {
        case seq::SIDE_LEFT:
          if ((strip->flag & SEQ_LEFTSEL) && (neighbor->flag & SEQ_RIGHTSEL)) {
            strip->flag |= SELECT;
            select_active_side(scene,
                               ed->seqbasep,
                               seq::SIDE_LEFT,
                               strip->channel,
                               seq::time_left_handle_frame_get(scene, strip));
          }
          else {
            strip->flag |= SELECT;
            neighbor->flag |= SELECT;
            recurs_sel_strip(neighbor);
            neighbor->flag |= SEQ_RIGHTSEL;
            strip->flag |= SEQ_LEFTSEL;
          }
          break;
        case seq::SIDE_RIGHT:
          if ((strip->flag & SEQ_RIGHTSEL) && (neighbor->flag & SEQ_LEFTSEL)) {
            strip->flag |= SELECT;
            select_active_side(scene,
                               ed->seqbasep,
                               seq::SIDE_RIGHT,
                               strip->channel,
                               seq::time_left_handle_frame_get(scene, strip));
          }
          else {
            strip->flag |= SELECT;
            neighbor->flag |= SELECT;
            recurs_sel_strip(neighbor);
            neighbor->flag |= SEQ_LEFTSEL;
            strip->flag |= SEQ_RIGHTSEL;
          }
          break;
      }
    }
    else {

      select_active_side(scene,
                         ed->seqbasep,
                         sel_side,
                         strip->channel,
                         seq::time_left_handle_frame_get(scene, strip));
    }
  }
}

/** Collect sequencer that are candidates for being selected. */
struct SeqSelect_Link {
  SeqSelect_Link *next, *prev;
  Strip *strip;
  /** Only use for center selection. */
  float center_dist_sq;
};

static int strip_sort_for_depth_select(const void *a, const void *b)
{
  const SeqSelect_Link *slink_a = static_cast<const SeqSelect_Link *>(a);
  const SeqSelect_Link *slink_b = static_cast<const SeqSelect_Link *>(b);

  /* Exactly overlapping strips, sort by channel (so the top-most is first). */
  if (slink_a->strip->channel < slink_b->strip->channel) {
    return 1;
  }
  if (slink_a->strip->channel > slink_b->strip->channel) {
    return -1;
  }
  return 0;
}

static int strip_sort_for_center_select(const void *a, const void *b)
{
  const SeqSelect_Link *slink_a = static_cast<const SeqSelect_Link *>(a);
  const SeqSelect_Link *slink_b = static_cast<const SeqSelect_Link *>(b);
  if (slink_a->center_dist_sq > slink_b->center_dist_sq) {
    return 1;
  }
  if (slink_a->center_dist_sq < slink_b->center_dist_sq) {
    return -1;
  }

  /* Exactly overlapping strips, use depth. */
  return strip_sort_for_depth_select(a, b);
}

/**
 * Check if click happened on image which belongs to strip.
 * If multiple strips are found, loop through them in order
 * (depth (top-most first) or closest to mouse when `center` is true).
 */
static Strip *strip_select_from_preview(
    const bContext *C, const int mval[2], const bool toggle, const bool extend, const bool center)
{
  Scene *scene = CTX_data_scene(C);
  Editing *ed = seq::editing_get(scene);
  ListBase *seqbase = seq::active_seqbase_get(ed);
  ListBase *channels = seq::channels_displayed_get(ed);
  SpaceSeq *sseq = CTX_wm_space_seq(C);
  View2D *v2d = UI_view2d_fromcontext(C);

  float mouseco_view[2];
  UI_view2d_region_to_view(v2d, mval[0], mval[1], &mouseco_view[0], &mouseco_view[1]);

  /* Always update the coordinates (check extended after). */
  const bool use_cycle = (!WM_cursor_test_motion_and_update(mval) || extend || toggle);

  /* Allow strips this far from the closest center to be included.
   * This allows cycling over center points which are near enough
   * to overlapping from the users perspective. */
  const float center_dist_sq_max = square_f(75.0f * U.pixelsize);
  const float center_scale_px[2] = {
      UI_view2d_scale_get_x(v2d),
      UI_view2d_scale_get_y(v2d),
  };

  blender::VectorSet strips = seq::query_rendered_strips(
      scene, channels, seqbase, scene->r.cfra, sseq->chanshown);

  SeqSelect_Link *slink_active = nullptr;
  Strip *strip_active = seq::select_active_get(scene);
  ListBase strips_ordered = {nullptr};
  for (Strip *strip : strips) {
    bool isect = false;
    float center_dist_sq_test = 0.0f;
    if (center) {
      /* Detect overlapping center points (scaled by the zoom level). */
      blender::float2 co = seq::image_transform_origin_offset_pixelspace_get(scene, strip);
      sub_v2_v2(co, mouseco_view);
      mul_v2_v2(co, center_scale_px);
      center_dist_sq_test = len_squared_v2(co);
      isect = center_dist_sq_test <= center_dist_sq_max;
      if (isect) {
        /* Use an active strip penalty for "center" selection when cycle is enabled. */
        if (use_cycle && (strip == strip_active) && (strip_active->flag & SELECT)) {
          center_dist_sq_test = square_f(sqrtf(center_dist_sq_test) + (3.0f * U.pixelsize));
        }
      }
    }
    else {
      isect = strip_point_image_isect(scene, strip, mouseco_view);
    }

    if (isect) {
      SeqSelect_Link *slink = MEM_callocN<SeqSelect_Link>(__func__);
      slink->strip = strip;
      slink->center_dist_sq = center_dist_sq_test;
      BLI_addtail(&strips_ordered, slink);

      if (strip == strip_active) {
        slink_active = slink;
      }
    }
  }

  BLI_listbase_sort(&strips_ordered,
                    center ? strip_sort_for_center_select : strip_sort_for_depth_select);

  SeqSelect_Link *slink_select = static_cast<SeqSelect_Link *>(strips_ordered.first);
  Strip *strip_select = nullptr;
  if (slink_select != nullptr) {
    /* Only use special behavior for the active strip when it's selected. */
    if ((center == false) && slink_active && (strip_active->flag & SELECT)) {
      if (use_cycle) {
        if (slink_active->next) {
          slink_select = slink_active->next;
        }
      }
      else {
        /* Match object selection behavior: keep the current active item unless cycle is enabled.
         * Clicking again in the same location will cycle away from the active object. */
        slink_select = slink_active;
      }
    }
    strip_select = slink_select->strip;
  }

  BLI_freelistN(&strips_ordered);

  return strip_select;
}

bool handle_is_selected(const Strip *strip, const eStripHandle handle)
{
  return ((strip->flag & SEQ_LEFTSEL) && (handle == STRIP_HANDLE_LEFT)) ||
         ((strip->flag & SEQ_RIGHTSEL) && (handle == STRIP_HANDLE_RIGHT));
}

/**
 * Test to see if the desired strip `selection` already matches the underlying strips' state.
 * If so, `sequencer_select` functions will keep the rest of the current timeline selection intact
 * on press, only selecting the given strip on release if no tweak occurs.
 */
static bool element_already_selected(const StripSelection &selection)
{
  if (selection.strip1 == nullptr) {
    return false;
  }

  const bool strip1_already_selected = ((selection.strip1->flag & SELECT) != 0);
  if (selection.strip2 == nullptr) {
    if (selection.handle == STRIP_HANDLE_NONE) {
      return strip1_already_selected && !(selection.strip1->flag & (SEQ_LEFTSEL | SEQ_RIGHTSEL));
    }
    return strip1_already_selected && handle_is_selected(selection.strip1, selection.handle);
  }

  /* If we are here, the strip selection is dual handle. */
  const bool strip2_already_selected = ((selection.strip2->flag & SELECT) != 0);
  const int strip1_handle = selection.strip1->flag & (SEQ_RIGHTSEL | SEQ_LEFTSEL);
  const int strip2_handle = selection.strip2->flag & (SEQ_RIGHTSEL | SEQ_LEFTSEL);
  /* Handles must be selected in XOR fashion, with `strip1` matching `handle_clicked`. */
  const bool both_handles_selected = strip1_handle == selection.handle && strip2_handle != 0 &&
                                     strip1_handle != strip2_handle;
  return strip1_already_selected && strip2_already_selected && both_handles_selected;
}

static void sequencer_select_connected_strips(const StripSelection &selection)
{
  blender::VectorSet<Strip *> sources;
  sources.add(selection.strip1);
  if (selection.strip2) {
    sources.add(selection.strip2);
  }

  for (Strip *source : sources) {
    blender::VectorSet<Strip *> connections = seq::connected_strips_get(source);
    for (Strip *connection : connections) {
      /* Copy selection settings exactly for connected strips. */
      connection->flag &= ~STRIP_ALLSEL;
      connection->flag |= source->flag & (STRIP_ALLSEL);
    }
  }
}

static void sequencer_copy_handles_to_selected_strips(const Scene *scene,
                                                      const StripSelection &selection,
                                                      VectorSet<Strip *> copy_to)
{
  /* TODO(john): Dual handle propagation is not supported for now due to its complexity,
   * but once we simplify selection assumptions in 5.0 we can add support for it. */
  if (selection.strip2) {
    return;
  }

  Strip *source = selection.strip1;
  /* Test for neighboring strips in the `copy_to` list. If any border one another, remove them,
   * since we don't want to mess with dual handles. */
  blender::VectorSet<Strip *> test(copy_to);
  test.add(source);
  for (Strip *test_strip : test) {
    /* Don't copy left handle over to a `test_strip` that has a strip directly on its left. */
    if ((source->flag & SEQ_LEFTSEL) &&
        find_neighboring_strip(scene, test_strip, seq::SIDE_LEFT, -1))
    {
      /* If this was the source strip, do not copy handles at all and prematurely return. */
      if (test_strip == source) {
        return;
      }
      copy_to.remove(test_strip);
    }

    /* Don't copy right handle over to a `test_strip` that has a strip directly on its right. */
    if ((source->flag & SEQ_RIGHTSEL) &&
        find_neighboring_strip(scene, test_strip, seq::SIDE_RIGHT, -1))
    {
      /* If this was the source strip, do not copy handles at all and prematurely return. */
      if (test_strip == source) {
        return;
      }
      copy_to.remove(test_strip);
    }
  }

  for (Strip *strip : copy_to) {
    /* NOTE that this can be `ALLSEL` since `prev_selection` was deselected earlier. */
    strip->flag &= ~STRIP_ALLSEL;
    strip->flag |= source->flag & STRIP_ALLSEL;
  }
}

static void sequencer_select_strip_impl(const Editing *ed,
                                        Strip *strip,
                                        const eStripHandle handle_clicked,
                                        const bool extend,
                                        const bool deselect,
                                        const bool toggle)
{
  const bool is_active = (ed->act_strip == strip);

  /* Exception for active strip handles. */
  if ((handle_clicked != STRIP_HANDLE_NONE) && (strip->flag & SELECT) && is_active && toggle) {
    if (handle_clicked == STRIP_HANDLE_LEFT) {
      strip->flag ^= SEQ_LEFTSEL;
    }
    else if (handle_clicked == STRIP_HANDLE_RIGHT) {
      strip->flag ^= SEQ_RIGHTSEL;
    }
    return;
  }

  /* Select strip. */
  /* Match object selection behavior. */
  int action = -1;
  if (extend) {
    action = 1;
  }
  else if (deselect) {
    action = 0;
  }
  else {
    if (!((strip->flag & SELECT) && is_active)) {
      action = 1;
    }
    else if (toggle) {
      action = 0;
    }
  }

  if (action == 1) {
    strip->flag |= SELECT;
    if (handle_clicked == STRIP_HANDLE_LEFT) {
      strip->flag |= SEQ_LEFTSEL;
    }
    if (handle_clicked == STRIP_HANDLE_RIGHT) {
      strip->flag |= SEQ_RIGHTSEL;
    }
  }
  else if (action == 0) {
    strip->flag &= ~STRIP_ALLSEL;
  }
}

static void select_linked_time(const Scene *scene,
                               const StripSelection &selection,
                               const bool extend,
                               const bool deselect,
                               const bool toggle)
{
  Editing *ed = seq::editing_get(scene);

  sequencer_select_strip_impl(ed, selection.strip1, selection.handle, extend, deselect, toggle);
  select_linked_time_strip(scene, selection.strip1, selection.handle);

  if (selection.strip2 != nullptr) {
    eStripHandle strip2_handle_clicked = (selection.handle == STRIP_HANDLE_LEFT) ?
                                             STRIP_HANDLE_RIGHT :
                                             STRIP_HANDLE_LEFT;
    sequencer_select_strip_impl(
        ed, selection.strip2, strip2_handle_clicked, extend, deselect, toggle);
    select_linked_time_strip(scene, selection.strip2, strip2_handle_clicked);
  }
}

/**
 * Similar to `strip_handle_draw_size_get()`, but returns a larger clickable area that is
 * the same for a given zoom level no matter whether "simplified tweaking" is turned off or on.
 * `strip_clickable_areas_get` will pad this past strip bounds by 1/3 of the inner handle size,
 * making the full handle size either 15 + 5 = 20px or 1/4 + 1/12 = 1/3 of the strip size.
 */
static float inner_clickable_handle_size_get(const Scene *scene,
                                             const Strip *strip,
                                             const View2D *v2d)
{
  const float pixelx = 1 / UI_view2d_scale_get_x(v2d);
  const float strip_len = seq::time_right_handle_frame_get(scene, strip) -
                          seq::time_left_handle_frame_get(scene, strip);
  return min_ff(15.0f * pixelx * U.pixelsize, strip_len / 4);
}

bool can_select_handle(const Scene *scene, const Strip *strip, const View2D *v2d)
{
  if (seq::effect_get_num_inputs(strip->type) > 0) {
    return false;
  }

  Editing *ed = seq::editing_get(scene);
  ListBase *channels = seq::channels_displayed_get(ed);
  if (seq::transform_is_locked(channels, strip)) {
    return false;
  }

  /* This ensures clickable handles are deactivated when the strip gets too small (25 or 15
   * frames). Since the full handle size for a small strip is 1/3 of the strip size (see
   * `inner_clickable_handle_size_get`), this means handles cannot be smaller than 25/3 = 8px for
   * simple tweaking, 15/3 = 5px for legacy behavior. */
  int min_len = 25 * U.pixelsize;
  if ((U.sequencer_editor_flag & USER_SEQ_ED_SIMPLE_TWEAKING) == 0) {
    min_len = 15 * U.pixelsize;
  }

  const float pixelx = 1 / UI_view2d_scale_get_x(v2d);
  const int strip_len = seq::time_right_handle_frame_get(scene, strip) -
                        seq::time_left_handle_frame_get(scene, strip);
  if (strip_len / pixelx < min_len) {
    return false;
  }

  if (UI_view2d_scale_get_y(v2d) < 16 * U.pixelsize) {
    return false;
  }

  return true;
}

static void strip_clickable_areas_get(const Scene *scene,
                                      const Strip *strip,
                                      const View2D *v2d,
                                      rctf *r_body,
                                      rctf *r_left_handle,
                                      rctf *r_right_handle)
{
  strip_rectf(scene, strip, r_body);
  *r_left_handle = *r_body;
  *r_right_handle = *r_body;

  const float handsize = inner_clickable_handle_size_get(scene, strip, v2d);
  r_left_handle->xmax = r_body->xmin + handsize;
  r_right_handle->xmin = r_body->xmax - handsize;
  BLI_rctf_pad(r_left_handle, handsize / 3, 0.0f);
  BLI_rctf_pad(r_right_handle, handsize / 3, 0.0f);
  BLI_rctf_pad(r_body, -handsize, 0.0f);
}

static rctf strip_clickable_area_get(const Scene *scene, const View2D *v2d, const Strip *strip)
{
  rctf body, left, right;
  strip_clickable_areas_get(scene, strip, v2d, &body, &left, &right);
  BLI_rctf_union(&body, &left);
  BLI_rctf_union(&body, &right);
  return body;
}

static float strip_to_frame_distance(const Scene *scene,
                                     const View2D *v2d,
                                     const Strip *strip,
                                     float timeline_frame)
{
  rctf body, left, right;
  strip_clickable_areas_get(scene, strip, v2d, &body, &left, &right);
  return BLI_rctf_length_x(&body, timeline_frame);
}

/**
 * Get strips that can be selected by a click from `mouse_co` in view-space.
 * The area considered includes padded handles past strip bounds, so multiple strips may be
 * returned.
 */
static blender::Vector<Strip *> padded_strips_under_mouse_get(const Scene *scene,
                                                              const View2D *v2d,
                                                              float mouse_co[2])
{
  Editing *ed = seq::editing_get(scene);

  if (ed == nullptr) {
    return {};
  }

  blender::Vector<Strip *> strips;
  LISTBASE_FOREACH (Strip *, strip, ed->seqbasep) {
    if (strip->channel != int(mouse_co[1])) {
      continue;
    }
    if (seq::time_left_handle_frame_get(scene, strip) > v2d->cur.xmax) {
      continue;
    }
    if (seq::time_right_handle_frame_get(scene, strip) < v2d->cur.xmin) {
      continue;
    }
    const rctf body = strip_clickable_area_get(scene, v2d, strip);
    if (!BLI_rctf_isect_pt_v(&body, mouse_co)) {
      continue;
    }
    strips.append(strip);
  }

  std::sort(strips.begin(), strips.end(), [&](const Strip *strip1, const Strip *strip2) {
    return strip_to_frame_distance(scene, v2d, strip1, mouse_co[0]) <
           strip_to_frame_distance(scene, v2d, strip2, mouse_co[0]);
  });

  return strips;
}

static bool strips_are_adjacent(const Scene *scene, const Strip *strip1, const Strip *strip2)
{
  const int s1_left = seq::time_left_handle_frame_get(scene, strip1);
  const int s1_right = seq::time_right_handle_frame_get(scene, strip1);
  const int s2_left = seq::time_left_handle_frame_get(scene, strip2);
  const int s2_right = seq::time_right_handle_frame_get(scene, strip2);

  return s1_right == s2_left || s1_left == s2_right;
}

static eStripHandle strip_handle_under_cursor_get(const Scene *scene,
                                                  const Strip *strip,
                                                  const View2D *v2d,
                                                  float mouse_co[2])
{
  if (!can_select_handle(scene, strip, v2d)) {
    return STRIP_HANDLE_NONE;
  }

  rctf body, left, right;
  strip_clickable_areas_get(scene, strip, v2d, &body, &left, &right);
  if (BLI_rctf_isect_pt_v(&left, mouse_co)) {
    return STRIP_HANDLE_LEFT;
  }
  if (BLI_rctf_isect_pt_v(&right, mouse_co)) {
    return STRIP_HANDLE_RIGHT;
  }

  return STRIP_HANDLE_NONE;
}

static bool is_mouse_over_both_handles_of_adjacent_strips(const Scene *scene,
                                                          blender::Vector<Strip *> strips,
                                                          const View2D *v2d,
                                                          float mouse_co[2])
{
  const eStripHandle strip1_handle = strip_handle_under_cursor_get(
      scene, strips[0], v2d, mouse_co);

  if (strip1_handle == STRIP_HANDLE_NONE) {
    return false;
  }
  if (!strips_are_adjacent(scene, strips[0], strips[1])) {
    return false;
  }
  const eStripHandle strip2_handle = strip_handle_under_cursor_get(
      scene, strips[1], v2d, mouse_co);
  if (strip1_handle == STRIP_HANDLE_RIGHT && strip2_handle != STRIP_HANDLE_LEFT) {
    return false;
  }
  if (strip1_handle == STRIP_HANDLE_LEFT && strip2_handle != STRIP_HANDLE_RIGHT) {
    return false;
  }

  return true;
}

StripSelection pick_strip_and_handle(const Scene *scene, const View2D *v2d, float mouse_co[2])
{
  StripSelection selection;
  /* Do not pick strips when clicking inside time scrub region. */
  float time_scrub_y = v2d->cur.ymax - UI_TIME_SCRUB_MARGIN_Y / UI_view2d_scale_get_y(v2d);
  if (mouse_co[1] > time_scrub_y) {
    return selection;
  }

  blender::Vector<Strip *> strips = padded_strips_under_mouse_get(scene, v2d, mouse_co);

  if (strips.size() == 0) {
    return selection;
  }

  selection.strip1 = strips[0];
  selection.handle = strip_handle_under_cursor_get(scene, selection.strip1, v2d, mouse_co);

  if (strips.size() == 2 && (U.sequencer_editor_flag & USER_SEQ_ED_SIMPLE_TWEAKING) != 0 &&
      is_mouse_over_both_handles_of_adjacent_strips(scene, strips, v2d, mouse_co))
  {
    selection.strip2 = strips[1];
  }

  return selection;
}

wmOperatorStatus sequencer_select_exec(bContext *C, wmOperator *op)
{
  const View2D *v2d = UI_view2d_fromcontext(C);
  Scene *scene = CTX_data_scene(C);
  Editing *ed = seq::editing_get(scene);
  ARegion *region = CTX_wm_region(C);
  ScrArea *area = CTX_wm_area(C);

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

  if (STREQ(area->runtime.tool->idname, "builtin.blade")) {
    /* Blade tool overrides select operator everywhere except the padded part of handles. We
     * should not be able to select these with this tool active, so return. */
    return OPERATOR_CANCELLED;
  }

  if (region->regiontype == RGN_TYPE_PREVIEW) {
    if (!sequencer_view_preview_only_poll(C)) {
      return OPERATOR_CANCELLED;
    }
    const SpaceSeq *sseq = CTX_wm_space_seq(C);
    if (sseq->mainb != SEQ_DRAW_IMG_IMBUF) {
      return OPERATOR_CANCELLED;
    }
  }

  const bool was_retiming = sequencer_retiming_mode_is_active(C);

  MouseCoords mouse_co(v2d, RNA_int_get(op->ptr, "mouse_x"), RNA_int_get(op->ptr, "mouse_y"));

  /* Check to see if the mouse cursor intersects with the retiming box; if so, `strip_key_owner` is
   * set. If the cursor intersects with a retiming key, `key` will be set too. */
  Strip *strip_key_owner = nullptr;
  SeqRetimingKey *key = retiming_mouseover_key_get(C, mouse_co.region, &strip_key_owner);

  /* If no key was found, the mouse cursor may still intersect with a "fake key" that has not been
   * realized yet. */
  if (strip_key_owner != nullptr && key == nullptr &&
      retiming_keys_can_be_displayed(CTX_wm_space_seq(C)) &&
      seq::retiming_data_is_editable(strip_key_owner))
  {
    key = try_to_realize_fake_keys(C, strip_key_owner, mouse_co.region);
  }

  if (key != nullptr) {
    if (!was_retiming) {
      deselect_all_strips(scene);
      sequencer_select_do_updates(C, scene);
    }
    /* Attempt to realize any other connected strips' fake keys. */
    if (seq::is_strip_connected(strip_key_owner)) {
      const int key_frame = seq::retiming_key_timeline_frame_get(scene, strip_key_owner, key);
      blender::VectorSet<Strip *> connections = seq::connected_strips_get(strip_key_owner);
      for (Strip *connection : connections) {
        if (key_frame == left_fake_key_frame_get(C, connection) ||
            key_frame == right_fake_key_frame_get(C, connection))
        {
          realize_fake_keys(scene, connection);
        }
      }
    }
    return sequencer_retiming_key_select_exec(C, op, key, strip_key_owner);
  }

  /* We should only reach here if no retiming selection is happening. */
  if (was_retiming) {
    seq::retiming_selection_clear(ed);
    WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER, scene);
  }

  const bool extend = RNA_boolean_get(op->ptr, "extend");
  const bool deselect = RNA_boolean_get(op->ptr, "deselect");
  const bool deselect_all = RNA_boolean_get(op->ptr, "deselect_all");
  const bool toggle = RNA_boolean_get(op->ptr, "toggle");
  const bool center = RNA_boolean_get(op->ptr, "center");

  StripSelection selection;
  if (region->regiontype == RGN_TYPE_PREVIEW) {
    selection.strip1 = strip_select_from_preview(C, mouse_co.region, toggle, extend, center);
  }
  else {
    selection = pick_strip_and_handle(scene, v2d, mouse_co.view);
  }

  /* NOTE: `side_of_frame` and `linked_time` functionality is designed to be shared on one
   * keymap, therefore both properties can be true at the same time. */
  if (selection.strip1 && RNA_boolean_get(op->ptr, "linked_time")) {
    if (!extend && !toggle) {
      deselect_all_strips(scene);
    }
    select_linked_time(scene, selection, extend, deselect, toggle);
    sequencer_select_do_updates(C, scene);
    sequencer_select_set_active(scene, selection.strip1);
    return OPERATOR_FINISHED;
  }

  /* Select left, right or overlapping the current frame. */
  if (RNA_boolean_get(op->ptr, "side_of_frame")) {
    if (!extend && !toggle) {
      deselect_all_strips(scene);
    }
    sequencer_select_side_of_frame(C, v2d, mouse_co.region, scene);
    sequencer_select_do_updates(C, scene);
    return OPERATOR_FINISHED;
  }

  /* On Alt selection, select the strip and bordering handles. */
  if (selection.strip1 && RNA_boolean_get(op->ptr, "linked_handle")) {
    if (!extend && !toggle) {
      deselect_all_strips(scene);
    }
    sequencer_select_linked_handle(C, selection.strip1, selection.handle);
    sequencer_select_do_updates(C, scene);
    sequencer_select_set_active(scene, selection.strip1);
    return OPERATOR_FINISHED;
  }

  const bool wait_to_deselect_others = RNA_boolean_get(op->ptr, "wait_to_deselect_others");
  const bool already_selected = element_already_selected(selection);

  SpaceSeq *sseq = CTX_wm_space_seq(C);
  if (selection.handle != STRIP_HANDLE_NONE && already_selected) {
    sseq->flag &= ~SPACE_SEQ_DESELECT_STRIP_HANDLE;
  }
  else {
    sseq->flag |= SPACE_SEQ_DESELECT_STRIP_HANDLE;
  }
  const bool ignore_connections = RNA_boolean_get(op->ptr, "ignore_connections");

  /* Clicking on already selected element falls on modal operation.
   * All strips are deselected on mouse button release unless extend mode is used. */
  if (already_selected && wait_to_deselect_others && !toggle && !ignore_connections) {
    return OPERATOR_RUNNING_MODAL;
  }

  VectorSet<Strip *> copy_to;
  /* True if the user selects either handle of a strip that is already selected, meaning that
   * handles should be propagated to all currently selected strips. */
  bool copy_handles_to_sel = (U.sequencer_editor_flag & USER_SEQ_ED_SIMPLE_TWEAKING) &&
                             (selection.handle != STRIP_HANDLE_NONE) &&
                             (selection.strip1->flag & SELECT);

  /* TODO(john): Dual handle propagation is not supported for now due to its complexity,
   * but once we simplify selection assumptions in 5.0 we can add support for it. */
  copy_handles_to_sel &= (selection.strip2 == nullptr);

  if (copy_handles_to_sel) {
    copy_to = seq::query_selected_strips(seq::active_seqbase_get(scene->ed));
    copy_to.remove(selection.strip1);
    copy_to.remove_if([](Strip *strip) { return (strip->type & STRIP_TYPE_EFFECT); });
  }

  bool changed = false;
  /* Deselect everything for now. NOTE that this condition runs for almost every click with no
   * modifiers. `sequencer_select_strip_impl` expects this and will re-select any strips in
   * `selection`. */
  if (deselect_all ||
      (selection.strip1 && (extend == false && deselect == false && toggle == false)))
  {
    changed |= deselect_all_strips(scene);
  }

  /* Nothing to select, but strips might have been deselected, in which case we should update. */
  if (!selection.strip1) {
    if (changed) {
      sequencer_select_do_updates(C, scene);
    }
    return changed ? OPERATOR_FINISHED : OPERATOR_CANCELLED;
  }

  /* Do actual selection. */
  sequencer_select_strip_impl(ed, selection.strip1, selection.handle, extend, deselect, toggle);
  if (selection.strip2 != nullptr) {
    /* Invert handle selection for second strip. */
    eStripHandle strip2_handle_clicked = (selection.handle == STRIP_HANDLE_LEFT) ?
                                             STRIP_HANDLE_RIGHT :
                                             STRIP_HANDLE_LEFT;
    sequencer_select_strip_impl(
        ed, selection.strip2, strip2_handle_clicked, extend, deselect, toggle);
  }

  if (!ignore_connections) {
    if (copy_handles_to_sel) {
      sequencer_copy_handles_to_selected_strips(scene, selection, copy_to);
    }

    sequencer_select_connected_strips(selection);
  }

  sequencer_select_do_updates(C, scene);
  sequencer_select_set_active(scene, selection.strip1);
  return OPERATOR_FINISHED;
}

static wmOperatorStatus sequencer_select_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
  const wmOperatorStatus retval = WM_generic_select_invoke(C, op, event);
  ARegion *region = CTX_wm_region(C);
  if (region && (region->regiontype == RGN_TYPE_PREVIEW)) {
    return WM_operator_flag_only_pass_through_on_press(retval, event);
  }
  return retval;
}

static std::string sequencer_select_get_name(wmOperatorType *ot, PointerRNA *ptr)
{
  if (RNA_boolean_get(ptr, "ignore_connections")) {
    return CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, "Select (Unconnected)");
  }
  if (RNA_boolean_get(ptr, "linked_time")) {
    return CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, "Select (Linked Time)");
  }
  if (RNA_boolean_get(ptr, "linked_handle")) {
    return CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, "Select (Linked Handle)");
  }
  if (RNA_boolean_get(ptr, "side_of_frame")) {
    return CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, "Select (Side of Frame)");
  }

  return ED_select_pick_get_name(ot, ptr);
}

void SEQUENCER_OT_select(wmOperatorType *ot)
{
  PropertyRNA *prop;

  /* Identifiers. */
  ot->name = "Select";
  ot->idname = "SEQUENCER_OT_select";
  ot->description = "Select a strip (last selected becomes the \"active strip\")";

  /* API callbacks. */
  ot->exec = sequencer_select_exec;
  ot->invoke = sequencer_select_invoke;
  ot->modal = WM_generic_select_modal;
  ot->poll = ED_operator_sequencer_active;
  ot->get_name = sequencer_select_get_name;

  /* Flags. */
  ot->flag = OPTYPE_UNDO;

  /* Properties. */
  WM_operator_properties_generic_select(ot);

  WM_operator_properties_mouse_select(ot);

  prop = RNA_def_boolean(
      ot->srna,
      "center",
      false,
      "Center",
      "Use the object center when selecting, in edit mode used to extend object selection");
  RNA_def_property_flag(prop, PROP_SKIP_SAVE);

  prop = RNA_def_boolean(ot->srna,
                         "linked_handle",
                         false,
                         "Linked Handle",
                         "Select handles next to the active strip");
  RNA_def_property_flag(prop, PROP_SKIP_SAVE);

  prop = RNA_def_boolean(ot->srna,
                         "linked_time",
                         false,
                         "Linked Time",
                         "Select other strips or handles at the same time, or all retiming keys "
                         "after the current in retiming mode");
  RNA_def_property_flag(prop, PROP_SKIP_SAVE);

  prop = RNA_def_boolean(
      ot->srna,
      "side_of_frame",
      false,
      "Side of Frame",
      "Select all strips on same side of the current frame as the mouse cursor");
  RNA_def_property_flag(prop, PROP_SKIP_SAVE);

  prop = RNA_def_boolean(ot->srna,
                         "ignore_connections",
                         false,
                         "Ignore Connections",
                         "Select strips individually whether or not they are connected");
  RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Select Handle Operator
 * \{ */

static wmOperatorStatus sequencer_select_handle_exec(bContext *C, wmOperator *op)
{
  /* This operator is only used in the RCS keymap by default and is not exposed in any menus. */
  const View2D *v2d = UI_view2d_fromcontext(C);
  Scene *scene = CTX_data_scene(C);
  Editing *ed = seq::editing_get(scene);

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

  if ((U.sequencer_editor_flag & USER_SEQ_ED_SIMPLE_TWEAKING) == 0) {
    return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
  }

  MouseCoords mouse_co(v2d, RNA_int_get(op->ptr, "mouse_x"), RNA_int_get(op->ptr, "mouse_y"));

  StripSelection selection = pick_strip_and_handle(scene, v2d, mouse_co.view);
  if (selection.strip1 == nullptr || selection.handle == STRIP_HANDLE_NONE) {
    return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
  }

  /* Ignore clicks on retiming keys. */
  Strip *strip_key_test = nullptr;
  SeqRetimingKey *key = retiming_mouseover_key_get(C, mouse_co.region, &strip_key_test);
  if (key != nullptr) {
    return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
  }

  SpaceSeq *sseq = CTX_wm_space_seq(C);
  if (element_already_selected(selection)) {
    sseq->flag &= ~SPACE_SEQ_DESELECT_STRIP_HANDLE;
    return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
  }
  sseq->flag |= SPACE_SEQ_DESELECT_STRIP_HANDLE;
  deselect_all_strips(scene);

  /* Do actual selection. */
  sequencer_select_strip_impl(ed, selection.strip1, selection.handle, false, false, false);
  if (selection.strip2 != nullptr) {
    /* Invert handle selection for second strip */
    eStripHandle strip2_handle_clicked = (selection.handle == STRIP_HANDLE_LEFT) ?
                                             STRIP_HANDLE_RIGHT :
                                             STRIP_HANDLE_LEFT;
    sequencer_select_strip_impl(ed, selection.strip2, strip2_handle_clicked, false, false, false);
  }

  const bool ignore_connections = RNA_boolean_get(op->ptr, "ignore_connections");
  if (!ignore_connections) {
    sequencer_select_connected_strips(selection);
  }

  seq::retiming_selection_clear(ed);
  sequencer_select_do_updates(C, scene);
  sequencer_select_set_active(scene, selection.strip1);
  return OPERATOR_FINISHED | OPERATOR_PASS_THROUGH;
}

static wmOperatorStatus sequencer_select_handle_invoke(bContext *C,
                                                       wmOperator *op,
                                                       const wmEvent *event)
{
  ARegion *region = CTX_wm_region(C);

  int mval[2];
  WM_event_drag_start_mval(event, region, mval);

  RNA_int_set(op->ptr, "mouse_x", mval[0]);
  RNA_int_set(op->ptr, "mouse_y", mval[1]);

  return sequencer_select_handle_exec(C, op);
}

void SEQUENCER_OT_select_handle(wmOperatorType *ot)
{
  PropertyRNA *prop;

  /* Identifiers. */
  ot->name = "Select Handle";
  ot->idname = "SEQUENCER_OT_select_handle";
  ot->description = "Select strip handle";

  /* API callbacks. */
  ot->exec = sequencer_select_handle_exec;
  ot->invoke = sequencer_select_handle_invoke;
  ot->poll = ED_operator_sequencer_active;

  /* Flags. */
  ot->flag = OPTYPE_UNDO;

  /* Properties. */
  WM_operator_properties_generic_select(ot);

  prop = RNA_def_boolean(ot->srna,
                         "ignore_connections",
                         false,
                         "Ignore Connections",
                         "Select strips individually whether or not they are connected");
  RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Select More Operator
 * \{ */

/* Run recursively to select linked. */
static bool select_linked_internal(Scene *scene)
{
  Editing *ed = seq::editing_get(scene);

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

  bool changed = false;

  LISTBASE_FOREACH (Strip *, strip, seq::active_seqbase_get(ed)) {
    if ((strip->flag & SELECT) == 0) {
      continue;
    }
    /* Only get unselected neighbors. */
    Strip *neighbor = find_neighboring_strip(scene, strip, seq::SIDE_LEFT, 0);
    if (neighbor) {
      neighbor->flag |= SELECT;
      recurs_sel_strip(neighbor);
      changed = true;
    }
    neighbor = find_neighboring_strip(scene, strip, seq::SIDE_RIGHT, 0);
    if (neighbor) {
      neighbor->flag |= SELECT;
      recurs_sel_strip(neighbor);
      changed = true;
    }
  }

  return changed;
}

/* Select only one linked strip on each side. */
static bool select_more_less_impl(Scene *scene, bool select_more)
{
  Editing *ed = seq::editing_get(scene);

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

  GSet *neighbors = BLI_gset_new(BLI_ghashutil_ptrhash, BLI_ghashutil_ptrcmp, "Linked strips");
  const int neighbor_selection_filter = select_more ? 0 : SELECT;
  const int selection_filter = select_more ? SELECT : 0;

  LISTBASE_FOREACH (Strip *, strip, seq::active_seqbase_get(ed)) {
    if ((strip->flag & SELECT) != selection_filter) {
      continue;
    }
    Strip *neighbor = find_neighboring_strip(
        scene, strip, seq::SIDE_LEFT, neighbor_selection_filter);
    if (neighbor) {
      BLI_gset_add(neighbors, neighbor);
    }
    neighbor = find_neighboring_strip(scene, strip, seq::SIDE_RIGHT, neighbor_selection_filter);
    if (neighbor) {
      BLI_gset_add(neighbors, neighbor);
    }
  }

  bool changed = false;
  GSetIterator gsi;
  BLI_gsetIterator_init(&gsi, neighbors);
  while (!BLI_gsetIterator_done(&gsi)) {
    Strip *neighbor = static_cast<Strip *>(BLI_gsetIterator_getKey(&gsi));
    if (select_more) {
      neighbor->flag |= SELECT;
      recurs_sel_strip(neighbor);
    }
    else {
      neighbor->flag &= ~SELECT;
    }
    changed = true;
    BLI_gsetIterator_step(&gsi);
  }

  BLI_gset_free(neighbors, nullptr);
  return changed;
}

static wmOperatorStatus sequencer_select_more_exec(bContext *C, wmOperator * /*op*/)
{
  Scene *scene = CTX_data_scene(C);

  if (!select_more_less_impl(scene, true)) {
    return OPERATOR_CANCELLED;
  }

  ED_outliner_select_sync_from_sequence_tag(C);

  WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER | NA_SELECTED, scene);

  return OPERATOR_FINISHED;
}

void SEQUENCER_OT_select_more(wmOperatorType *ot)
{
  /* Identifiers. */
  ot->name = "Select More";
  ot->idname = "SEQUENCER_OT_select_more";
  ot->description = "Select more strips adjacent to the current selection";

  /* API callbacks. */
  ot->exec = sequencer_select_more_exec;
  ot->poll = sequencer_edit_poll;

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

/** \} */

/* -------------------------------------------------------------------- */
/** \name Select Less Operator
 * \{ */

static wmOperatorStatus sequencer_select_less_exec(bContext *C, wmOperator * /*op*/)
{
  Scene *scene = CTX_data_scene(C);

  if (!select_more_less_impl(scene, false)) {
    return OPERATOR_CANCELLED;
  }

  ED_outliner_select_sync_from_sequence_tag(C);

  WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER | NA_SELECTED, scene);

  return OPERATOR_FINISHED;
}

void SEQUENCER_OT_select_less(wmOperatorType *ot)
{
  /* Identifiers. */
  ot->name = "Select Less";
  ot->idname = "SEQUENCER_OT_select_less";
  ot->description = "Shrink the current selection of adjacent selected strips";

  /* API callbacks. */
  ot->exec = sequencer_select_less_exec;
  ot->poll = sequencer_edit_poll;

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

/** \} */

/* -------------------------------------------------------------------- */
/** \name Select Pick Linked Operator
 * \{ */

static wmOperatorStatus sequencer_select_linked_pick_invoke(bContext *C,
                                                            wmOperator *op,
                                                            const wmEvent *event)
{
  Scene *scene = CTX_data_scene(C);
  const View2D *v2d = UI_view2d_fromcontext(C);

  bool extend = RNA_boolean_get(op->ptr, "extend");

  float mouse_co[2];
  UI_view2d_region_to_view(v2d, event->mval[0], event->mval[1], &mouse_co[0], &mouse_co[1]);

  /* This works like UV, not mesh. */
  StripSelection mouse_selection = pick_strip_and_handle(scene, v2d, mouse_co);
  if (!mouse_selection.strip1) {
    return OPERATOR_FINISHED; /* User error as with mesh?? */
  }

  if (extend == 0) {
    deselect_all_strips(scene);
  }

  mouse_selection.strip1->flag |= SELECT;
  recurs_sel_strip(mouse_selection.strip1);

  bool selected = true;
  while (selected) {
    selected = select_linked_internal(scene);
  }

  ED_outliner_select_sync_from_sequence_tag(C);

  WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER | NA_SELECTED, scene);

  return OPERATOR_FINISHED;
}

void SEQUENCER_OT_select_linked_pick(wmOperatorType *ot)
{
  /* Identifiers. */
  ot->name = "Select Pick Linked";
  ot->idname = "SEQUENCER_OT_select_linked_pick";
  ot->description = "Select a chain of linked strips nearest to the mouse pointer";

  /* API callbacks. */
  ot->invoke = sequencer_select_linked_pick_invoke;
  ot->poll = ED_operator_sequencer_active;

  /* Flags. */
  ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

  /* Properties. */
  PropertyRNA *prop;
  prop = RNA_def_boolean(ot->srna, "extend", false, "Extend", "Extend the selection");
  RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Select Linked Operator
 * \{ */

static wmOperatorStatus sequencer_select_linked_exec(bContext *C, wmOperator * /*op*/)
{
  Scene *scene = CTX_data_scene(C);
  bool selected;

  selected = true;
  while (selected) {
    selected = select_linked_internal(scene);
  }

  ED_outliner_select_sync_from_sequence_tag(C);

  WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER | NA_SELECTED, scene);

  return OPERATOR_FINISHED;
}

void SEQUENCER_OT_select_linked(wmOperatorType *ot)
{
  /* Identifiers. */
  ot->name = "Select Linked";
  ot->idname = "SEQUENCER_OT_select_linked";
  ot->description = "Select all strips adjacent to the current selection";

  /* API callbacks. */
  ot->exec = sequencer_select_linked_exec;
  ot->poll = sequencer_edit_poll;

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

/** \} */

/* -------------------------------------------------------------------- */
/** \name Select Handles Operator
 * \{ */

enum {
  SEQ_SELECT_HANDLES_SIDE_LEFT,
  SEQ_SELECT_HANDLES_SIDE_RIGHT,
  SEQ_SELECT_HANDLES_SIDE_BOTH,
  SEQ_SELECT_HANDLES_SIDE_LEFT_NEIGHBOR,
  SEQ_SELECT_HANDLES_SIDE_RIGHT_NEIGHBOR,
  SEQ_SELECT_HANDLES_SIDE_BOTH_NEIGHBORS,
};

static const EnumPropertyItem prop_select_handles_side_types[] = {
    {SEQ_SELECT_HANDLES_SIDE_LEFT, "LEFT", 0, "Left", ""},
    {SEQ_SELECT_HANDLES_SIDE_RIGHT, "RIGHT", 0, "Right", ""},
    {SEQ_SELECT_HANDLES_SIDE_BOTH, "BOTH", 0, "Both", ""},
    {SEQ_SELECT_HANDLES_SIDE_LEFT_NEIGHBOR, "LEFT_NEIGHBOR", 0, "Left Neighbor", ""},
    {SEQ_SELECT_HANDLES_SIDE_RIGHT_NEIGHBOR, "RIGHT_NEIGHBOR", 0, "Right Neighbor", ""},
    {SEQ_SELECT_HANDLES_SIDE_BOTH_NEIGHBORS, "BOTH_NEIGHBORS", 0, "Both Neighbors", ""},
    {0, nullptr, 0, nullptr, nullptr},
};

static wmOperatorStatus sequencer_select_handles_exec(bContext *C, wmOperator *op)
{
  Scene *scene = CTX_data_scene(C);
  Editing *ed = seq::editing_get(scene);
  int sel_side = RNA_enum_get(op->ptr, "side");
  LISTBASE_FOREACH (Strip *, strip, ed->seqbasep) {
    if (strip->flag & SELECT) {
      Strip *l_neighbor = find_neighboring_strip(scene, strip, seq::SIDE_LEFT, -1);
      Strip *r_neighbor = find_neighboring_strip(scene, strip, seq::SIDE_RIGHT, -1);

      switch (sel_side) {
        case SEQ_SELECT_HANDLES_SIDE_LEFT:
          strip->flag &= ~SEQ_RIGHTSEL;
          strip->flag |= SEQ_LEFTSEL;
          break;
        case SEQ_SELECT_HANDLES_SIDE_RIGHT:
          strip->flag &= ~SEQ_LEFTSEL;
          strip->flag |= SEQ_RIGHTSEL;
          break;
        case SEQ_SELECT_HANDLES_SIDE_BOTH:
          strip->flag |= SEQ_LEFTSEL | SEQ_RIGHTSEL;
          break;
        case SEQ_SELECT_HANDLES_SIDE_LEFT_NEIGHBOR:
          if (l_neighbor) {
            if (!(l_neighbor->flag & SELECT)) {
              l_neighbor->flag |= SEQ_RIGHTSEL;
            }
          }
          break;
        case SEQ_SELECT_HANDLES_SIDE_RIGHT_NEIGHBOR:
          if (r_neighbor) {
            if (!(r_neighbor->flag & SELECT)) {
              r_neighbor->flag |= SEQ_LEFTSEL;
            }
          }
          break;
        case SEQ_SELECT_HANDLES_SIDE_BOTH_NEIGHBORS:
          if (l_neighbor) {
            if (!(l_neighbor->flag & SELECT)) {
              l_neighbor->flag |= SEQ_RIGHTSEL;
            }
          }
          if (r_neighbor) {
            if (!(r_neighbor->flag & SELECT)) {
              r_neighbor->flag |= SEQ_LEFTSEL;
            }
            break;
          }
      }
    }
  }
  /*   Select strips */
  LISTBASE_FOREACH (Strip *, strip, ed->seqbasep) {
    if ((strip->flag & SEQ_LEFTSEL) || (strip->flag & SEQ_RIGHTSEL)) {
      if (!(strip->flag & SELECT)) {
        strip->flag |= SELECT;
        recurs_sel_strip(strip);
      }
    }
  }

  ED_outliner_select_sync_from_sequence_tag(C);

  WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER | NA_SELECTED, scene);

  return OPERATOR_FINISHED;
}

void SEQUENCER_OT_select_handles(wmOperatorType *ot)
{
  /* Identifiers. */
  ot->name = "Select Handles";
  ot->idname = "SEQUENCER_OT_select_handles";
  ot->description = "Select gizmo handles on the sides of the selected strip";

  /* API callbacks. */
  ot->exec = sequencer_select_handles_exec;
  ot->poll = sequencer_edit_poll;

  /* Flags. */
  ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

  /* Properties. */
  RNA_def_enum(ot->srna,
               "side",
               prop_select_handles_side_types,
               SEQ_SELECT_HANDLES_SIDE_BOTH,
               "Side",
               "The side of the handle that is selected");
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Select Side of Frame Operator
 * \{ */

static wmOperatorStatus sequencer_select_side_of_frame_exec(bContext *C, wmOperator *op)
{
  Scene *scene = CTX_data_scene(C);
  Editing *ed = seq::editing_get(scene);
  const bool extend = RNA_boolean_get(op->ptr, "extend");
  const int side = RNA_enum_get(op->ptr, "side");

  if (ed == nullptr) {
    return OPERATOR_CANCELLED;
  }
  if (extend == false) {
    deselect_all_strips(scene);
  }
  const int timeline_frame = scene->r.cfra;
  LISTBASE_FOREACH (Strip *, strip, seq::active_seqbase_get(ed)) {
    bool test = false;
    switch (side) {
      case -1:
        test = (timeline_frame >= seq::time_right_handle_frame_get(scene, strip));
        break;
      case 1:
        test = (timeline_frame <= seq::time_left_handle_frame_get(scene, strip));
        break;
      case 2:
        test = seq::time_strip_intersects_frame(scene, strip, timeline_frame);
        break;
    }

    if (test) {
      strip->flag |= SELECT;
      recurs_sel_strip(strip);
    }
  }

  ED_outliner_select_sync_from_sequence_tag(C);

  WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER | NA_SELECTED, scene);

  return OPERATOR_FINISHED;
}

void SEQUENCER_OT_select_side_of_frame(wmOperatorType *ot)
{
  static const EnumPropertyItem sequencer_select_left_right_types[] = {
      {-1, "LEFT", 0, "Left", "Select to the left of the current frame"},
      {1, "RIGHT", 0, "Right", "Select to the right of the current frame"},
      {2, "CURRENT", 0, "Current Frame", "Select intersecting with the current frame"},
      {0, nullptr, 0, nullptr, nullptr},
  };

  /* Identifiers. */
  ot->name = "Select Side of Frame";
  ot->idname = "SEQUENCER_OT_select_side_of_frame";
  ot->description = "Select strips relative to the current frame";

  /* API callbacks. */
  ot->exec = sequencer_select_side_of_frame_exec;
  ot->poll = ED_operator_sequencer_active;

  /* Flags. */
  ot->flag = OPTYPE_UNDO;

  /* Properties. */
  PropertyRNA *prop;
  prop = RNA_def_boolean(ot->srna, "extend", false, "Extend", "Extend the selection");
  RNA_def_property_flag(prop, PROP_SKIP_SAVE);
  ot->prop = RNA_def_enum(ot->srna, "side", sequencer_select_left_right_types, 0, "Side", "");
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Select Side Operator
 * \{ */

static wmOperatorStatus sequencer_select_side_exec(bContext *C, wmOperator *op)
{
  Scene *scene = CTX_data_scene(C);
  Editing *ed = seq::editing_get(scene);

  const int sel_side = RNA_enum_get(op->ptr, "side");
  const int frame_init = sel_side == seq::SIDE_LEFT ? INT_MIN : INT_MAX;
  int frame_ranges[seq::MAX_CHANNELS];
  bool selected = false;

  copy_vn_i(frame_ranges, ARRAY_SIZE(frame_ranges), frame_init);

  LISTBASE_FOREACH (Strip *, strip, ed->seqbasep) {
    if (UNLIKELY(strip->channel >= seq::MAX_CHANNELS)) {
      continue;
    }
    int *frame_limit_p = &frame_ranges[strip->channel];
    if (strip->flag & SELECT) {
      selected = true;
      if (sel_side == seq::SIDE_LEFT) {
        *frame_limit_p = max_ii(*frame_limit_p, seq::time_left_handle_frame_get(scene, strip));
      }
      else {
        *frame_limit_p = min_ii(*frame_limit_p, seq::time_left_handle_frame_get(scene, strip));
      }
    }
  }

  if (selected == false) {
    return OPERATOR_CANCELLED;
  }

  select_active_side_range(scene, ed->seqbasep, sel_side, frame_ranges, frame_init);

  ED_outliner_select_sync_from_sequence_tag(C);

  WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER | NA_SELECTED, scene);

  return OPERATOR_FINISHED;
}

void SEQUENCER_OT_select_side(wmOperatorType *ot)
{
  /* Identifiers. */
  ot->name = "Select Side";
  ot->idname = "SEQUENCER_OT_select_side";
  ot->description = "Select strips on the nominated side of the selected strips";

  /* API callbacks. */
  ot->exec = sequencer_select_side_exec;
  ot->poll = sequencer_edit_poll;

  /* Flags. */
  ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

  /* Properties. */
  RNA_def_enum(ot->srna,
               "side",
               prop_side_types,
               seq::SIDE_BOTH,
               "Side",
               "The side to which the selection is applied");
}

/** \} */

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

static bool strip_box_select_rect_image_isect(const Scene *scene,
                                              const Strip *strip,
                                              const rctf *rect)
{
  const blender::Array<blender::float2> strip_image_quad = seq::image_transform_final_quad_get(
      scene, strip);
  float rect_quad[4][2] = {{rect->xmax, rect->ymax},
                           {rect->xmax, rect->ymin},
                           {rect->xmin, rect->ymin},
                           {rect->xmin, rect->ymax}};

  return strip_point_image_isect(scene, strip, rect_quad[0]) ||
         strip_point_image_isect(scene, strip, rect_quad[1]) ||
         strip_point_image_isect(scene, strip, rect_quad[2]) ||
         strip_point_image_isect(scene, strip, rect_quad[3]) ||
         isect_point_quad_v2(
             strip_image_quad[0], rect_quad[0], rect_quad[1], rect_quad[2], rect_quad[3]) ||
         isect_point_quad_v2(
             strip_image_quad[1], rect_quad[0], rect_quad[1], rect_quad[2], rect_quad[3]) ||
         isect_point_quad_v2(
             strip_image_quad[2], rect_quad[0], rect_quad[1], rect_quad[2], rect_quad[3]) ||
         isect_point_quad_v2(
             strip_image_quad[3], rect_quad[0], rect_quad[1], rect_quad[2], rect_quad[3]);
}

static void seq_box_select_strip_from_preview(const bContext *C,
                                              const rctf *rect,
                                              const eSelectOp mode)
{
  Scene *scene = CTX_data_scene(C);
  Editing *ed = seq::editing_get(scene);
  ListBase *seqbase = seq::active_seqbase_get(ed);
  ListBase *channels = seq::channels_displayed_get(ed);
  SpaceSeq *sseq = CTX_wm_space_seq(C);

  blender::VectorSet strips = seq::query_rendered_strips(
      scene, channels, seqbase, scene->r.cfra, sseq->chanshown);
  for (Strip *strip : strips) {
    if (!strip_box_select_rect_image_isect(scene, strip, rect)) {
      continue;
    }

    if (ELEM(mode, SEL_OP_ADD, SEL_OP_SET)) {
      strip->flag |= SELECT;
    }
    else {
      BLI_assert(mode == SEL_OP_SUB);
      strip->flag &= ~SELECT;
    }
  }
}

static wmOperatorStatus sequencer_box_select_exec(bContext *C, wmOperator *op)
{
  Scene *scene = CTX_data_scene(C);
  View2D *v2d = UI_view2d_fromcontext(C);
  Editing *ed = seq::editing_get(scene);

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

  if (sequencer_retiming_mode_is_active(C) && retiming_keys_can_be_displayed(CTX_wm_space_seq(C)))
  {
    return sequencer_retiming_box_select_exec(C, op);
  }

  const eSelectOp sel_op = eSelectOp(RNA_enum_get(op->ptr, "mode"));
  const bool handles = RNA_boolean_get(op->ptr, "include_handles");
  const bool select = (sel_op != SEL_OP_SUB);

  bool changed = false;

  if (SEL_OP_USE_PRE_DESELECT(sel_op)) {
    changed |= deselect_all_strips(scene);
  }

  rctf rectf;
  WM_operator_properties_border_to_rctf(op, &rectf);
  UI_view2d_region_to_view_rctf(v2d, &rectf, &rectf);

  ARegion *region = CTX_wm_region(C);
  if (region->regiontype == RGN_TYPE_PREVIEW) {
    if (!sequencer_view_preview_only_poll(C)) {
      return OPERATOR_CANCELLED;
    }
    seq_box_select_strip_from_preview(C, &rectf, sel_op);
    sequencer_select_do_updates(C, scene);
    return OPERATOR_FINISHED;
  }

  LISTBASE_FOREACH (Strip *, strip, ed->seqbasep) {
    rctf rq;
    strip_rectf(scene, strip, &rq);
    if (BLI_rctf_isect(&rq, &rectf, nullptr)) {
      if (handles) {
        /* Get the clickable handle size, ignoring padding. */
        float handsize = inner_clickable_handle_size_get(scene, strip, v2d) * 4;

        /* Right handle. */
        if (rectf.xmax > (seq::time_right_handle_frame_get(scene, strip) - handsize)) {
          if (select) {
            strip->flag |= SELECT | SEQ_RIGHTSEL;
          }
          else {
            /* Deselect the strip if it's left with no handles selected. */
            if ((strip->flag & SEQ_RIGHTSEL) && ((strip->flag & SEQ_LEFTSEL) == 0)) {
              strip->flag &= ~SELECT;
            }
            strip->flag &= ~SEQ_RIGHTSEL;
          }

          changed = true;
        }
        /* Left handle. */
        if (rectf.xmin < (seq::time_left_handle_frame_get(scene, strip) + handsize)) {
          if (select) {
            strip->flag |= SELECT | SEQ_LEFTSEL;
          }
          else {
            /* Deselect the strip if it's left with no handles selected. */
            if ((strip->flag & SEQ_LEFTSEL) && ((strip->flag & SEQ_RIGHTSEL) == 0)) {
              strip->flag &= ~SELECT;
            }
            strip->flag &= ~SEQ_LEFTSEL;
          }
        }

        changed = true;
      }

      /* Regular box selection. */
      else {
        SET_FLAG_FROM_TEST(strip->flag, select, SELECT);
        strip->flag &= ~(SEQ_LEFTSEL | SEQ_RIGHTSEL);
        changed = true;
      }

      const bool ignore_connections = RNA_boolean_get(op->ptr, "ignore_connections");
      if (!ignore_connections) {
        /* Propagate selection to connected strips. */
        StripSelection selection;
        selection.strip1 = strip;
        sequencer_select_connected_strips(selection);
      }
    }
  }

  if (!changed) {
    return OPERATOR_CANCELLED;
  }

  sequencer_select_do_updates(C, scene);

  return OPERATOR_FINISHED;
}

static wmOperatorStatus sequencer_box_select_invoke(bContext *C,
                                                    wmOperator *op,
                                                    const wmEvent *event)
{
  Scene *scene = CTX_data_scene(C);
  const View2D *v2d = UI_view2d_fromcontext(C);
  ARegion *region = CTX_wm_region(C);

  if (region->regiontype == RGN_TYPE_PREVIEW && !sequencer_view_preview_only_poll(C)) {
    return OPERATOR_CANCELLED;
  }

  const bool tweak = RNA_boolean_get(op->ptr, "tweak");

  if (tweak) {
    int mval[2];
    float mouse_co[2];
    WM_event_drag_start_mval(event, region, mval);
    UI_view2d_region_to_view(v2d, mval[0], mval[1], &mouse_co[0], &mouse_co[1]);

    StripSelection selection = pick_strip_and_handle(scene, v2d, mouse_co);

    if (selection.strip1 != nullptr) {
      return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
    }
  }

  return WM_gesture_box_invoke(C, op, event);
}

void SEQUENCER_OT_select_box(wmOperatorType *ot)
{
  PropertyRNA *prop;

  /* Identifiers. */
  ot->name = "Box Select";
  ot->idname = "SEQUENCER_OT_select_box";
  ot->description = "Select strips using box selection";

  /* API callbacks. */
  ot->invoke = sequencer_box_select_invoke;
  ot->exec = sequencer_box_select_exec;
  ot->modal = WM_gesture_box_modal;
  ot->cancel = WM_gesture_box_cancel;

  ot->poll = ED_operator_sequencer_active;

  /* Flags. */
  ot->flag = OPTYPE_UNDO;

  /* Properties. */
  WM_operator_properties_gesture_box(ot);
  WM_operator_properties_select_operation_simple(ot);

  prop = RNA_def_boolean(
      ot->srna,
      "tweak",
      false,
      "Tweak",
      "Make box select pass through to sequence slide when the cursor is hovering on a strip");
  RNA_def_property_flag(prop, PROP_SKIP_SAVE);

  prop = RNA_def_boolean(
      ot->srna, "include_handles", false, "Select Handles", "Select the strips and their handles");
  RNA_def_property_flag(prop, PROP_SKIP_SAVE);

  prop = RNA_def_boolean(ot->srna,
                         "ignore_connections",
                         false,
                         "Ignore Connections",
                         "Select strips individually whether or not they are connected");
  RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Select Grouped Operator
 * \{ */

enum {
  SEQ_SELECT_GROUP_TYPE,
  SEQ_SELECT_GROUP_TYPE_BASIC,
  SEQ_SELECT_GROUP_TYPE_EFFECT,
  SEQ_SELECT_GROUP_DATA,
  SEQ_SELECT_GROUP_EFFECT,
  SEQ_SELECT_GROUP_EFFECT_LINK,
  SEQ_SELECT_GROUP_OVERLAP,
};

static const EnumPropertyItem sequencer_prop_select_grouped_types[] = {
    {SEQ_SELECT_GROUP_TYPE, "TYPE", 0, "Type", "Shared strip type"},
    {SEQ_SELECT_GROUP_TYPE_BASIC,
     "TYPE_BASIC",
     0,
     "Global Type",
     "All strips of same basic type (graphical or sound)"},
    {SEQ_SELECT_GROUP_TYPE_EFFECT,
     "TYPE_EFFECT",
     0,
     "Effect Type",
     "Shared strip effect type (if active strip is not an effect one, select all non-effect "
     "strips)"},
    {SEQ_SELECT_GROUP_DATA, "DATA", 0, "Data", "Shared data (scene, image, sound, etc.)"},
    {SEQ_SELECT_GROUP_EFFECT, "EFFECT", 0, "Effect", "Shared effects"},
    {SEQ_SELECT_GROUP_EFFECT_LINK,
     "EFFECT_LINK",
     0,
     "Effect/Linked",
     "Other strips affected by the active one (sharing some time, and below or "
     "effect-assigned)"},
    {SEQ_SELECT_GROUP_OVERLAP, "OVERLAP", 0, "Overlap", "Overlapping time"},
    {0, nullptr, 0, nullptr, nullptr},
};

#define STRIP_IS_SOUND(_strip) \
  ((_strip->type & STRIP_TYPE_SOUND_RAM) && !(_strip->type & STRIP_TYPE_EFFECT))

#define STRIP_IS_EFFECT(_strip) ((_strip->type & STRIP_TYPE_EFFECT) != 0)

#define STRIP_USE_DATA(_strip) \
  (ELEM(_strip->type, STRIP_TYPE_SCENE, STRIP_TYPE_MOVIECLIP, STRIP_TYPE_MASK) || \
   STRIP_HAS_PATH(_strip))

#define STRIP_CHANNEL_CHECK(_strip, _chan) ELEM((_chan), 0, (_strip)->channel)

static bool select_grouped_type(blender::Span<Strip *> strips,
                                ListBase * /*seqbase*/,
                                Strip *act_strip,
                                const int channel)
{
  bool changed = false;

  for (Strip *strip : strips) {
    if (STRIP_CHANNEL_CHECK(strip, channel) && strip->type == act_strip->type) {
      strip->flag |= SELECT;
      changed = true;
    }
  }

  return changed;
}

static bool select_grouped_type_basic(blender::Span<Strip *> strips,
                                      ListBase * /*seqbase*/,
                                      Strip *act_strip,
                                      const int channel)
{
  bool changed = false;
  const bool is_sound = STRIP_IS_SOUND(act_strip);

  for (Strip *strip : strips) {
    if (STRIP_CHANNEL_CHECK(strip, channel) &&
        (is_sound ? STRIP_IS_SOUND(strip) : !STRIP_IS_SOUND(strip)))
    {
      strip->flag |= SELECT;
      changed = true;
    }
  }

  return changed;
}

static bool select_grouped_type_effect(blender::Span<Strip *> strips,
                                       ListBase * /*seqbase*/,
                                       Strip *act_strip,
                                       const int channel)
{
  bool changed = false;
  const bool is_effect = STRIP_IS_EFFECT(act_strip);

  for (Strip *strip : strips) {
    if (STRIP_CHANNEL_CHECK(strip, channel) &&
        (is_effect ? STRIP_IS_EFFECT(strip) : !STRIP_IS_EFFECT(strip)))
    {
      strip->flag |= SELECT;
      changed = true;
    }
  }

  return changed;
}

static bool select_grouped_data(blender::Span<Strip *> strips,
                                ListBase * /*seqbase*/,
                                Strip *act_strip,
                                const int channel)
{
  bool changed = false;
  const char *dirpath = act_strip->data ? act_strip->data->dirpath : nullptr;

  if (!STRIP_USE_DATA(act_strip)) {
    return changed;
  }

  if (STRIP_HAS_PATH(act_strip) && dirpath) {
    for (Strip *strip : strips) {
      if (STRIP_CHANNEL_CHECK(strip, channel) && STRIP_HAS_PATH(strip) && strip->data &&
          STREQ(strip->data->dirpath, dirpath))
      {
        strip->flag |= SELECT;
        changed = true;
      }
    }
  }
  else if (act_strip->type == STRIP_TYPE_SCENE) {
    Scene *sce = act_strip->scene;
    for (Strip *strip : strips) {
      if (STRIP_CHANNEL_CHECK(strip, channel) && strip->type == STRIP_TYPE_SCENE &&
          strip->scene == sce)
      {
        strip->flag |= SELECT;
        changed = true;
      }
    }
  }
  else if (act_strip->type == STRIP_TYPE_MOVIECLIP) {
    MovieClip *clip = act_strip->clip;
    for (Strip *strip : strips) {
      if (STRIP_CHANNEL_CHECK(strip, channel) && strip->type == STRIP_TYPE_MOVIECLIP &&
          strip->clip == clip)
      {
        strip->flag |= SELECT;
        changed = true;
      }
    }
  }
  else if (act_strip->type == STRIP_TYPE_MASK) {
    Mask *mask = act_strip->mask;
    for (Strip *strip : strips) {
      if (STRIP_CHANNEL_CHECK(strip, channel) && strip->type == STRIP_TYPE_MASK &&
          strip->mask == mask)
      {
        strip->flag |= SELECT;
        changed = true;
      }
    }
  }

  return changed;
}

static bool select_grouped_effect(blender::Span<Strip *> strips,
                                  ListBase * /*seqbase*/,
                                  Strip *act_strip,
                                  const int channel)
{
  bool changed = false;
  bool effects[STRIP_TYPE_MAX + 1];

  for (int i = 0; i <= STRIP_TYPE_MAX; i++) {
    effects[i] = false;
  }

  for (Strip *strip : strips) {
    if (STRIP_CHANNEL_CHECK(strip, channel) && (strip->type & STRIP_TYPE_EFFECT) &&
        seq::relation_is_effect_of_strip(strip, act_strip))
    {
      effects[strip->type] = true;
    }
  }

  for (Strip *strip : strips) {
    if (STRIP_CHANNEL_CHECK(strip, channel) && effects[strip->type]) {
      if (strip->input1) {
        strip->input1->flag |= SELECT;
      }
      if (strip->input2) {
        strip->input2->flag |= SELECT;
      }
      changed = true;
    }
  }

  return changed;
}

static bool select_grouped_time_overlap(const Scene *scene,
                                        blender::Span<Strip *> strips,
                                        ListBase * /*seqbase*/,
                                        Strip *act_strip)
{
  bool changed = false;

  for (Strip *strip : strips) {
    if (seq::time_left_handle_frame_get(scene, strip) <
            seq::time_right_handle_frame_get(scene, act_strip) &&
        seq::time_right_handle_frame_get(scene, strip) >
            seq::time_left_handle_frame_get(scene, act_strip))
    {
      strip->flag |= SELECT;
      changed = true;
    }
  }

  return changed;
}

/* Query strips that are in lower channel and intersect in time with strip_reference. */
static void query_lower_channel_strips(const Scene *scene,
                                       Strip *strip_reference,
                                       ListBase *seqbase,
                                       blender::VectorSet<Strip *> &strips)
{
  LISTBASE_FOREACH (Strip *, strip_test, seqbase) {
    if (strip_test->channel > strip_reference->channel) {
      continue; /* Not lower channel. */
    }
    if (seq::time_right_handle_frame_get(scene, strip_test) <=
            seq::time_left_handle_frame_get(scene, strip_reference) ||
        seq::time_left_handle_frame_get(scene, strip_test) >=
            seq::time_right_handle_frame_get(scene, strip_reference))
    {
      continue; /* Not intersecting in time. */
    }
    strips.add(strip_test);
  }
}

/* Select all strips within time range and with lower channel of initial selection. Then select
 * effect chains of these strips. */
static bool select_grouped_effect_link(const Scene *scene,
                                       blender::VectorSet<Strip *> strips,
                                       ListBase *seqbase,
                                       Strip * /*act_strip*/,
                                       const int /*channel*/)
{
  /* Get collection of strips. */
  strips.remove_if([&](Strip *strip) { return (strip->flag & SELECT) == 0; });
  const int selected_strip_count = strips.size();
  /* XXX: this uses scene as arg, so it does not work with iterator :( I had thought about this,
   * but expand function is just so useful... I can just add scene and inject it I guess. */
  seq::iterator_set_expand(scene, seqbase, strips, query_lower_channel_strips);
  seq::iterator_set_expand(scene, seqbase, strips, seq::query_strip_effect_chain);

  /* Check if other strips will be affected. */
  const bool changed = strips.size() > selected_strip_count;

  /* Actual logic. */
  for (Strip *strip : strips) {
    strip->flag |= SELECT;
  }

  return changed;
}

#undef STRIP_IS_SOUND
#undef STRIP_IS_EFFECT
#undef STRIP_USE_DATA

static wmOperatorStatus sequencer_select_grouped_exec(bContext *C, wmOperator *op)
{
  Scene *scene = CTX_data_scene(C);
  ListBase *seqbase = seq::active_seqbase_get(seq::editing_get(scene));
  Strip *act_strip = seq::select_active_get(scene);

  const bool is_preview = sequencer_view_has_preview_poll(C);
  if (is_preview && !sequencer_view_preview_only_poll(C)) {
    return OPERATOR_CANCELLED;
  }

  blender::VectorSet strips = all_strips_from_context(C);

  if (act_strip == nullptr || (is_preview && !strips.contains(act_strip))) {
    BKE_report(op->reports, RPT_ERROR, "No active strip!");
    return OPERATOR_CANCELLED;
  }

  const int type = RNA_enum_get(op->ptr, "type");
  const int channel = RNA_boolean_get(op->ptr, "use_active_channel") ? act_strip->channel : 0;
  const bool extend = RNA_boolean_get(op->ptr, "extend");

  bool changed = false;

  if (!extend) {
    LISTBASE_FOREACH (Strip *, strip, seqbase) {
      strip->flag &= ~SELECT;
      changed = true;
    }
  }

  switch (type) {
    case SEQ_SELECT_GROUP_TYPE:
      changed |= select_grouped_type(strips, seqbase, act_strip, channel);
      break;
    case SEQ_SELECT_GROUP_TYPE_BASIC:
      changed |= select_grouped_type_basic(strips, seqbase, act_strip, channel);
      break;
    case SEQ_SELECT_GROUP_TYPE_EFFECT:
      changed |= select_grouped_type_effect(strips, seqbase, act_strip, channel);
      break;
    case SEQ_SELECT_GROUP_DATA:
      changed |= select_grouped_data(strips, seqbase, act_strip, channel);
      break;
    case SEQ_SELECT_GROUP_EFFECT:
      changed |= select_grouped_effect(strips, seqbase, act_strip, channel);
      break;
    case SEQ_SELECT_GROUP_EFFECT_LINK:
      changed |= select_grouped_effect_link(scene, strips, seqbase, act_strip, channel);
      break;
    case SEQ_SELECT_GROUP_OVERLAP:
      changed |= select_grouped_time_overlap(scene, strips, seqbase, act_strip);
      break;
    default:
      BLI_assert(0);
      break;
  }

  if (changed) {
    ED_outliner_select_sync_from_sequence_tag(C);
    WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER | NA_SELECTED, scene);
    return OPERATOR_FINISHED;
  }

  return OPERATOR_CANCELLED;
}

void SEQUENCER_OT_select_grouped(wmOperatorType *ot)
{
  /* Identifiers. */
  ot->name = "Select Grouped";
  ot->idname = "SEQUENCER_OT_select_grouped";
  ot->description = "Select all strips grouped by various properties";

  /* API callbacks. */
  ot->invoke = WM_menu_invoke;
  ot->exec = sequencer_select_grouped_exec;
  ot->poll = sequencer_edit_poll;

  /* Flags. */
  ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

  /* Properties. */
  ot->prop = RNA_def_enum(ot->srna, "type", sequencer_prop_select_grouped_types, 0, "Type", "");
  RNA_def_boolean(ot->srna,
                  "extend",
                  false,
                  "Extend",
                  "Extend selection instead of deselecting everything first");
  RNA_def_boolean(ot->srna,
                  "use_active_channel",
                  false,
                  "Same Channel",
                  "Only consider strips on the same channel as the active one");
}

/** \} */

}  // namespace blender::ed::vse
