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

/** \file
 * \ingroup edtransform
 */

#include "DNA_anim_types.h"
#include "DNA_gpencil_legacy_types.h"
#include "DNA_mask_types.h"

#include "MEM_guardedalloc.h"

#include "BLI_listbase.h"
#include "BLI_math.h"
#include "BLI_rect.h"

#include "BKE_context.h"
#include "BKE_fcurve.h"
#include "BKE_gpencil_legacy.h"
#include "BKE_key.h"
#include "BKE_layer.h"
#include "BKE_mask.h"
#include "BKE_nla.h"

#include "ED_anim_api.h"
#include "ED_keyframes_edit.h"
#include "ED_markers.h"

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

#include "transform.hh"
#include "transform_snap.hh"

#include "transform_convert.hh"

/* helper struct for gp-frame transforms */
struct tGPFtransdata {
  union {
    float val;    /* where transdata writes transform */
    float loc[3]; /* #td->val and #td->loc share the same pointer. */
  };
  int *sdata; /* pointer to gpf->framenum */
};

/* -------------------------------------------------------------------- */
/** \name Action Transform Creation
 * \{ */

/* fully select selected beztriples, but only include if it's on the right side of cfra */
static int count_fcurve_keys(FCurve *fcu, char side, float cfra, bool is_prop_edit)
{
  BezTriple *bezt;
  int i, count = 0, count_all = 0;

  if (ELEM(nullptr, fcu, fcu->bezt)) {
    return count;
  }

  /* only include points that occur on the right side of cfra */
  for (i = 0, bezt = fcu->bezt; i < fcu->totvert; i++, bezt++) {
    if (FrameOnMouseSide(side, bezt->vec[1][0], cfra)) {
      /* no need to adjust the handle selection since they are assumed
       * selected (like graph editor with SIPO_NOHANDLES) */
      if (bezt->f2 & SELECT) {
        count++;
      }

      count_all++;
    }
  }

  if (is_prop_edit && count > 0) {
    return count_all;
  }
  return count;
}

/* fully select selected beztriples, but only include if it's on the right side of cfra */
static int count_gplayer_frames(bGPDlayer *gpl, char side, float cfra, bool is_prop_edit)
{
  bGPDframe *gpf;
  int count = 0, count_all = 0;

  if (gpl == nullptr) {
    return count;
  }

  /* only include points that occur on the right side of cfra */
  for (gpf = static_cast<bGPDframe *>(gpl->frames.first); gpf; gpf = gpf->next) {
    if (FrameOnMouseSide(side, float(gpf->framenum), cfra)) {
      if (gpf->flag & GP_FRAME_SELECT) {
        count++;
      }
      count_all++;
    }
  }

  if (is_prop_edit && count > 0) {
    return count_all;
  }
  return count;
}

/* fully select selected beztriples, but only include if it's on the right side of cfra */
static int count_masklayer_frames(MaskLayer *masklay, char side, float cfra, bool is_prop_edit)
{
  MaskLayerShape *masklayer_shape;
  int count = 0, count_all = 0;

  if (masklay == nullptr) {
    return count;
  }

  /* only include points that occur on the right side of cfra */
  for (masklayer_shape = static_cast<MaskLayerShape *>(masklay->splines_shapes.first);
       masklayer_shape;
       masklayer_shape = masklayer_shape->next)
  {
    if (FrameOnMouseSide(side, float(masklayer_shape->frame), cfra)) {
      if (masklayer_shape->flag & MASK_SHAPE_SELECT) {
        count++;
      }
      count_all++;
    }
  }

  if (is_prop_edit && count > 0) {
    return count_all;
  }
  return count;
}

/* This function assigns the information to transdata */
static void TimeToTransData(
    TransData *td, TransData2D *td2d, BezTriple *bezt, AnimData *adt, float ypos)
{
  float *time = bezt->vec[1];

  /* Setup #TransData2D. */
  td2d->loc[0] = *time;
  td2d->loc2d = time;
  td2d->h1 = bezt->vec[0];
  td2d->h2 = bezt->vec[2];
  copy_v2_v2(td2d->ih1, td2d->h1);
  copy_v2_v2(td2d->ih2, td2d->h2);

  /* Setup #TransData. */

  /* Usually #td2d->loc is used here.
   * But this is for when the original location is not float[3]. */
  td->loc = time;

  copy_v3_v3(td->iloc, td->loc);
  td->val = time;
  td->ival = *(time);
  td->center[0] = td->ival;
  td->center[1] = ypos;

  /* Store the AnimData where this keyframe exists as a keyframe of the
   * active action as #td->extra. */
  td->extra = adt;

  if (bezt->f2 & SELECT) {
    td->flag |= TD_SELECTED;
  }

  /* Set flags to move handles as necessary. */
  td->flag |= TD_MOVEHANDLE1 | TD_MOVEHANDLE2;
}

/* This function advances the address to which td points to, so it must return
 * the new address so that the next time new transform data is added, it doesn't
 * overwrite the existing ones...  i.e.   td = IcuToTransData(td, icu, ob, side, cfra);
 *
 * The 'side' argument is needed for the extend mode. 'B' = both sides, 'R'/'L' mean only data
 * on the named side are used.
 */
static TransData *ActionFCurveToTransData(TransData *td,
                                          TransData2D **td2dv,
                                          FCurve *fcu,
                                          AnimData *adt,
                                          char side,
                                          float cfra,
                                          bool is_prop_edit,
                                          float ypos)
{
  BezTriple *bezt;
  TransData2D *td2d = *td2dv;
  int i;

  if (ELEM(nullptr, fcu, fcu->bezt)) {
    return td;
  }

  for (i = 0, bezt = fcu->bezt; i < fcu->totvert; i++, bezt++) {
    /* only add selected keyframes (for now, proportional edit is not enabled) */
    if (is_prop_edit || (bezt->f2 & SELECT))
    { /* note this MUST match count_fcurve_keys(), * so can't use BEZT_ISSEL_ANY() macro */
      /* only add if on the right 'side' of the current frame */
      if (FrameOnMouseSide(side, bezt->vec[1][0], cfra)) {
        TimeToTransData(td, td2d, bezt, adt, ypos);

        td++;
        td2d++;
      }
    }
  }

  *td2dv = td2d;

  return td;
}

/**
 * This function advances the address to which td points to, so it must return
 * the new address so that the next time new transform data is added, it doesn't
 * overwrite the existing ones: e.g. `td += GPLayerToTransData(td, ...);`
 *
 * \param side: is needed for the extend mode. 'B' = both sides,
 * 'R'/'L' mean only data on the named side are used.
 */
static int GPLayerToTransData(TransData *td,
                              tGPFtransdata *tfd,
                              bGPDlayer *gpl,
                              char side,
                              float cfra,
                              bool is_prop_edit,
                              float ypos)
{
  bGPDframe *gpf;
  int count = 0;

  /* check for select frames on right side of current frame */
  for (gpf = static_cast<bGPDframe *>(gpl->frames.first); gpf; gpf = gpf->next) {
    const bool is_selected = (gpf->flag & GP_FRAME_SELECT) != 0;
    if (is_prop_edit || is_selected) {
      if (FrameOnMouseSide(side, float(gpf->framenum), cfra)) {
        tfd->val = float(gpf->framenum);
        tfd->sdata = &gpf->framenum;

        td->val = td->loc = &tfd->val;
        td->ival = td->iloc[0] = tfd->val;

        td->center[0] = td->ival;
        td->center[1] = ypos;

        if (is_selected) {
          td->flag = TD_SELECTED;
        }

        /* Advance `td` now. */
        td++;
        tfd++;
        count++;
      }
    }
  }

  return count;
}

/* refer to comment above #GPLayerToTransData, this is the same but for masks */
static int MaskLayerToTransData(TransData *td,
                                tGPFtransdata *tfd,
                                MaskLayer *masklay,
                                char side,
                                float cfra,
                                bool is_prop_edit,
                                float ypos)
{
  MaskLayerShape *masklay_shape;
  int count = 0;

  /* check for select frames on right side of current frame */
  for (masklay_shape = static_cast<MaskLayerShape *>(masklay->splines_shapes.first); masklay_shape;
       masklay_shape = masklay_shape->next)
  {
    if (is_prop_edit || (masklay_shape->flag & MASK_SHAPE_SELECT)) {
      if (FrameOnMouseSide(side, float(masklay_shape->frame), cfra)) {
        tfd->val = float(masklay_shape->frame);
        tfd->sdata = &masklay_shape->frame;

        td->val = td->loc = &tfd->val;
        td->ival = td->iloc[0] = tfd->val;

        td->center[0] = td->ival;
        td->center[1] = ypos;

        /* advance td now */
        td++;
        tfd++;
        count++;
      }
    }
  }

  return count;
}

static void createTransActionData(bContext *C, TransInfo *t)
{
  Scene *scene = t->scene;
  TransData *td = nullptr;
  TransData2D *td2d = nullptr;
  tGPFtransdata *tfd = nullptr;

  rcti *mask = &t->region->v2d.mask;
  rctf *datamask = &t->region->v2d.cur;

  float xsize = BLI_rctf_size_x(datamask);
  float ysize = BLI_rctf_size_y(datamask);
  float xmask = BLI_rcti_size_x(mask);
  float ymask = BLI_rcti_size_y(mask);

  bAnimContext ac;
  ListBase anim_data = {nullptr, nullptr};
  bAnimListElem *ale;
  int filter;
  const bool is_prop_edit = (t->flag & T_PROP_EDIT) != 0;

  int count = 0;
  int gpf_count = 0;
  float cfra;
  float ypos = 1.0f / ((ysize / xsize) * (xmask / ymask)) * BLI_rctf_cent_y(&t->region->v2d.cur);

  /* determine what type of data we are operating on */
  if (ANIM_animdata_get_context(C, &ac) == 0) {
    return;
  }

  /* filter data */
  filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_FOREDIT);
  ANIM_animdata_filter(
      &ac, &anim_data, eAnimFilter_Flags(filter), ac.data, eAnimCont_Types(ac.datatype));

  /* which side of the current frame should be allowed */
  if (t->mode == TFM_TIME_EXTEND) {
    t->frame_side = transform_convert_frame_side_dir_get(t, float(scene->r.cfra));
  }
  else {
    /* normal transform - both sides of current frame are considered */
    t->frame_side = 'B';
  }

  /* loop 1: fully select F-Curve keys and count how many BezTriples are selected */
  for (ale = static_cast<bAnimListElem *>(anim_data.first); ale; ale = ale->next) {
    AnimData *adt = ANIM_nla_mapping_get(&ac, ale);
    int adt_count = 0;
    /* convert current-frame to action-time (slightly less accurate, especially under
     * higher scaling ratios, but is faster than converting all points)
     */
    if (adt) {
      cfra = BKE_nla_tweakedit_remap(adt, float(scene->r.cfra), NLATIME_CONVERT_UNMAP);
    }
    else {
      cfra = float(scene->r.cfra);
    }

    if (ELEM(ale->type, ANIMTYPE_FCURVE, ANIMTYPE_NLACURVE)) {
      adt_count = count_fcurve_keys(
          static_cast<FCurve *>(ale->key_data), t->frame_side, cfra, is_prop_edit);
    }
    else if (ale->type == ANIMTYPE_GPLAYER) {
      adt_count = count_gplayer_frames(
          static_cast<bGPDlayer *>(ale->data), t->frame_side, cfra, is_prop_edit);
    }
    else if (ale->type == ANIMTYPE_MASKLAYER) {
      adt_count = count_masklayer_frames(
          static_cast<MaskLayer *>(ale->data), t->frame_side, cfra, is_prop_edit);
    }
    else {
      BLI_assert(0);
    }

    if (adt_count > 0) {
      if (ELEM(ale->type, ANIMTYPE_GPLAYER, ANIMTYPE_MASKLAYER)) {
        gpf_count += adt_count;
      }
      count += adt_count;
      ale->tag = true;
    }
  }

  /* stop if trying to build list if nothing selected */
  if (count == 0 && gpf_count == 0) {
    /* cleanup temp list */
    ANIM_animdata_freelist(&anim_data);
    return;
  }

  TransDataContainer *tc = TRANS_DATA_CONTAINER_FIRST_SINGLE(t);

  /* allocate memory for data */
  tc->data_len = count;

  tc->data = static_cast<TransData *>(
      MEM_callocN(tc->data_len * sizeof(TransData), "TransData(Action Editor)"));
  tc->data_2d = static_cast<TransData2D *>(
      MEM_callocN(tc->data_len * sizeof(TransData2D), "transdata2d"));
  td = tc->data;
  td2d = tc->data_2d;

  if (ELEM(ac.datatype, ANIMCONT_GPENCIL, ANIMCONT_MASK, ANIMCONT_DOPESHEET, ANIMCONT_TIMELINE)) {
    tc->data_gpf_len = gpf_count;
    tc->custom.type.data = tfd = static_cast<tGPFtransdata *>(
        MEM_callocN(sizeof(tGPFtransdata) * gpf_count, "tGPFtransdata"));
    tc->custom.type.use_free = true;
  }

  /* loop 2: build transdata array */
  for (ale = static_cast<bAnimListElem *>(anim_data.first); ale; ale = ale->next) {

    if (is_prop_edit && !ale->tag) {
      continue;
    }

    cfra = float(scene->r.cfra);

    {
      AnimData *adt;
      adt = ANIM_nla_mapping_get(&ac, ale);
      if (adt) {
        cfra = BKE_nla_tweakedit_remap(adt, cfra, NLATIME_CONVERT_UNMAP);
      }
    }

    if (ale->type == ANIMTYPE_GPLAYER) {
      bGPDlayer *gpl = (bGPDlayer *)ale->data;
      int i;

      i = GPLayerToTransData(td, tfd, gpl, t->frame_side, cfra, is_prop_edit, ypos);
      td += i;
      tfd += i;
    }
    else if (ale->type == ANIMTYPE_MASKLAYER) {
      MaskLayer *masklay = (MaskLayer *)ale->data;
      int i;

      i = MaskLayerToTransData(td, tfd, masklay, t->frame_side, cfra, is_prop_edit, ypos);
      td += i;
      tfd += i;
    }
    else {
      AnimData *adt = ANIM_nla_mapping_get(&ac, ale);
      FCurve *fcu = (FCurve *)ale->key_data;

      td = ActionFCurveToTransData(td, &td2d, fcu, adt, t->frame_side, cfra, is_prop_edit, ypos);
    }
  }

  /* calculate distances for proportional editing */
  if (is_prop_edit) {
    td = tc->data;

    for (ale = static_cast<bAnimListElem *>(anim_data.first); ale; ale = ale->next) {
      AnimData *adt;

      /* F-Curve may not have any keyframes */
      if (!ale->tag) {
        continue;
      }

      adt = ANIM_nla_mapping_get(&ac, ale);
      if (adt) {
        cfra = BKE_nla_tweakedit_remap(adt, float(scene->r.cfra), NLATIME_CONVERT_UNMAP);
      }
      else {
        cfra = float(scene->r.cfra);
      }

      if (ale->type == ANIMTYPE_GPLAYER) {
        bGPDlayer *gpl = (bGPDlayer *)ale->data;
        bGPDframe *gpf;

        for (gpf = static_cast<bGPDframe *>(gpl->frames.first); gpf; gpf = gpf->next) {
          if (gpf->flag & GP_FRAME_SELECT) {
            td->dist = td->rdist = 0.0f;
          }
          else {
            bGPDframe *gpf_iter;
            int min = INT_MAX;
            for (gpf_iter = static_cast<bGPDframe *>(gpl->frames.first); gpf_iter;
                 gpf_iter = gpf_iter->next) {
              if (gpf_iter->flag & GP_FRAME_SELECT) {
                if (FrameOnMouseSide(t->frame_side, float(gpf_iter->framenum), cfra)) {
                  int val = abs(gpf->framenum - gpf_iter->framenum);
                  if (val < min) {
                    min = val;
                  }
                }
              }
            }
            td->dist = td->rdist = min;
          }
          td++;
        }
      }
      else if (ale->type == ANIMTYPE_MASKLAYER) {
        MaskLayer *masklay = (MaskLayer *)ale->data;
        MaskLayerShape *masklay_shape;

        for (masklay_shape = static_cast<MaskLayerShape *>(masklay->splines_shapes.first);
             masklay_shape;
             masklay_shape = masklay_shape->next)
        {
          if (FrameOnMouseSide(t->frame_side, float(masklay_shape->frame), cfra)) {
            if (masklay_shape->flag & MASK_SHAPE_SELECT) {
              td->dist = td->rdist = 0.0f;
            }
            else {
              MaskLayerShape *masklay_iter;
              int min = INT_MAX;
              for (masklay_iter = static_cast<MaskLayerShape *>(masklay->splines_shapes.first);
                   masklay_iter;
                   masklay_iter = masklay_iter->next)
              {
                if (masklay_iter->flag & MASK_SHAPE_SELECT) {
                  if (FrameOnMouseSide(t->frame_side, float(masklay_iter->frame), cfra)) {
                    int val = abs(masklay_shape->frame - masklay_iter->frame);
                    if (val < min) {
                      min = val;
                    }
                  }
                }
              }
              td->dist = td->rdist = min;
            }
            td++;
          }
        }
      }
      else {
        FCurve *fcu = (FCurve *)ale->key_data;
        BezTriple *bezt;
        int i;

        for (i = 0, bezt = fcu->bezt; i < fcu->totvert; i++, bezt++) {
          if (FrameOnMouseSide(t->frame_side, bezt->vec[1][0], cfra)) {
            if (bezt->f2 & SELECT) {
              td->dist = td->rdist = 0.0f;
            }
            else {
              BezTriple *bezt_iter;
              int j;
              float min = FLT_MAX;
              for (j = 0, bezt_iter = fcu->bezt; j < fcu->totvert; j++, bezt_iter++) {
                if (bezt_iter->f2 & SELECT) {
                  if (FrameOnMouseSide(t->frame_side, float(bezt_iter->vec[1][0]), cfra)) {
                    float val = fabs(bezt->vec[1][0] - bezt_iter->vec[1][0]);
                    if (val < min) {
                      min = val;
                    }
                  }
                }
              }
              td->dist = td->rdist = min;
            }
            td++;
          }
        }
      }
    }
  }

  /* cleanup temp list */
  ANIM_animdata_freelist(&anim_data);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Action Transform Flush
 * \{ */

/* This function helps flush transdata written to tempdata into the gp-frames. */
static void flushTransIntFrameActionData(TransInfo *t)
{
  TransDataContainer *tc = TRANS_DATA_CONTAINER_FIRST_SINGLE(t);
  tGPFtransdata *tfd = static_cast<tGPFtransdata *>(tc->custom.type.data);

  /* flush data!
   * Expects data_gpf_len to be set in the data container. */
  for (int i = 0; i < tc->data_gpf_len; i++, tfd++) {
    *(tfd->sdata) = round_fl_to_int(tfd->val);
  }
}

static void recalcData_actedit(TransInfo *t)
{
  ViewLayer *view_layer = t->view_layer;
  SpaceAction *saction = (SpaceAction *)t->area->spacedata.first;

  bAnimContext ac = {nullptr};
  ListBase anim_data = {nullptr, nullptr};
  bAnimListElem *ale;
  int filter;

  BKE_view_layer_synced_ensure(t->scene, t->view_layer);

  /* initialize relevant anim-context 'context' data from TransInfo data */
  /* NOTE: sync this with the code in ANIM_animdata_get_context() */
  ac.bmain = CTX_data_main(t->context);
  ac.scene = t->scene;
  ac.view_layer = t->view_layer;
  ac.obact = BKE_view_layer_active_object_get(view_layer);
  ac.area = t->area;
  ac.region = t->region;
  ac.sl = static_cast<SpaceLink *>((t->area) ? t->area->spacedata.first : nullptr);
  ac.spacetype = (t->area) ? t->area->spacetype : 0;
  ac.regiontype = (t->region) ? t->region->regiontype : 0;

  ANIM_animdata_context_getdata(&ac);

  /* perform flush */
  if (ELEM(ac.datatype, ANIMCONT_GPENCIL, ANIMCONT_MASK, ANIMCONT_DOPESHEET, ANIMCONT_TIMELINE)) {
    /* flush transform values back to actual coordinates */
    flushTransIntFrameActionData(t);
  }

  /* Flush 2d vector. */
  TransDataContainer *tc = TRANS_DATA_CONTAINER_FIRST_SINGLE(t);
  const short autosnap = getAnimEdit_SnapMode(t);
  TransData *td;
  TransData2D *td2d;
  int i = 0;
  for (td = tc->data, td2d = tc->data_2d; i < tc->data_len; i++, td++, td2d++) {
    if ((autosnap != SACTSNAP_OFF) && (t->state != TRANS_CANCEL) && !(td->flag & TD_NOTIMESNAP)) {
      transform_snap_anim_flush_data(t, td, eAnimEdit_AutoSnap(autosnap), td->loc);
    }

    /* Constrain Y. */
    td->loc[1] = td->iloc[1];

    transform_convert_flush_handle2D(td, td2d, 0.0f);
  }

  if (ac.datatype != ANIMCONT_MASK) {
    /* Get animdata blocks visible in editor,
     * assuming that these will be the ones where things changed. */
    filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_ANIMDATA);
    ANIM_animdata_filter(
        &ac, &anim_data, eAnimFilter_Flags(filter), ac.data, eAnimCont_Types(ac.datatype));

    /* just tag these animdata-blocks to recalc, assuming that some data there changed
     * BUT only do this if realtime updates are enabled
     */
    if ((saction->flag & SACTION_NOREALTIMEUPDATES) == 0) {
      for (ale = static_cast<bAnimListElem *>(anim_data.first); ale; ale = ale->next) {
        /* set refresh tags for objects using this animation */
        ANIM_list_elem_update(CTX_data_main(t->context), t->scene, ale);
      }
    }

    /* now free temp channels */
    ANIM_animdata_freelist(&anim_data);
  }
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Special After Transform Action
 * \{ */

static int masklay_shape_cmp_frame(void *thunk, const void *a, const void *b)
{
  const MaskLayerShape *frame_a = static_cast<const MaskLayerShape *>(a);
  const MaskLayerShape *frame_b = static_cast<const MaskLayerShape *>(b);

  if (frame_a->frame < frame_b->frame) {
    return -1;
  }
  if (frame_a->frame > frame_b->frame) {
    return 1;
  }
  *((bool *)thunk) = true;
  /* selected last */
  if ((frame_a->flag & MASK_SHAPE_SELECT) && ((frame_b->flag & MASK_SHAPE_SELECT) == 0)) {
    return 1;
  }
  return 0;
}

static void posttrans_mask_clean(Mask *mask)
{
  MaskLayer *masklay;

  for (masklay = static_cast<MaskLayer *>(mask->masklayers.first); masklay;
       masklay = masklay->next) {
    MaskLayerShape *masklay_shape, *masklay_shape_next;
    bool is_double = false;

    BLI_listbase_sort_r(&masklay->splines_shapes, masklay_shape_cmp_frame, &is_double);

    if (is_double) {
      for (masklay_shape = static_cast<MaskLayerShape *>(masklay->splines_shapes.first);
           masklay_shape;
           masklay_shape = masklay_shape_next)
      {
        masklay_shape_next = masklay_shape->next;
        if (masklay_shape_next && masklay_shape->frame == masklay_shape_next->frame) {
          BKE_mask_layer_shape_unlink(masklay, masklay_shape);
        }
      }
    }

#ifdef DEBUG
    for (masklay_shape = static_cast<MaskLayerShape *>(masklay->splines_shapes.first);
         masklay_shape;
         masklay_shape = masklay_shape->next)
    {
      BLI_assert(!masklay_shape->next || masklay_shape->frame < masklay_shape->next->frame);
    }
#endif
  }

  WM_main_add_notifier(NC_MASK | NA_EDITED, mask);
}

/* Called by special_aftertrans_update to make sure selected gp-frames replace
 * any other gp-frames which may reside on that frame (that are not selected).
 * It also makes sure gp-frames are still stored in chronological order after
 * transform.
 */
static void posttrans_gpd_clean(bGPdata *gpd)
{
  bGPDlayer *gpl;

  for (gpl = static_cast<bGPDlayer *>(gpd->layers.first); gpl; gpl = gpl->next) {
    bGPDframe *gpf, *gpfn;
    bool is_double = false;

    BKE_gpencil_layer_frames_sort(gpl, &is_double);

    if (is_double) {
      for (gpf = static_cast<bGPDframe *>(gpl->frames.first); gpf; gpf = gpfn) {
        gpfn = gpf->next;
        if (gpfn && gpf->framenum == gpfn->framenum) {
          BKE_gpencil_layer_frame_delete(gpl, gpf);
        }
      }
    }

#ifdef DEBUG
    for (gpf = static_cast<bGPDframe *>(gpl->frames.first); gpf; gpf = gpf->next) {
      BLI_assert(!gpf->next || gpf->framenum < gpf->next->framenum);
    }
#endif
  }
  /* set cache flag to dirty */
  DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);

  WM_main_add_notifier(NC_GPENCIL | NA_EDITED, gpd);
}

/* Called by special_aftertrans_update to make sure selected keyframes replace
 * any other keyframes which may reside on that frame (that is not selected).
 * remake_action_ipos should have already been called
 */
static void posttrans_action_clean(bAnimContext *ac, bAction *act)
{
  ListBase anim_data = {nullptr, nullptr};
  bAnimListElem *ale;
  int filter;

  /* filter data */
  filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_FOREDIT | ANIMFILTER_FCURVESONLY);
  ANIM_animdata_filter(ac, &anim_data, eAnimFilter_Flags(filter), act, ANIMCONT_ACTION);

  /* loop through relevant data, removing keyframes as appropriate
   *      - all keyframes are converted in/out of global time
   */
  for (ale = static_cast<bAnimListElem *>(anim_data.first); ale; ale = ale->next) {
    AnimData *adt = ANIM_nla_mapping_get(ac, ale);

    if (adt) {
      ANIM_nla_mapping_apply_fcurve(adt, static_cast<FCurve *>(ale->key_data), 0, 0);
      BKE_fcurve_merge_duplicate_keys(static_cast<FCurve *>(ale->key_data),
                                      SELECT,
                                      false); /* only use handles in graph editor */
      ANIM_nla_mapping_apply_fcurve(adt, static_cast<FCurve *>(ale->key_data), 1, 0);
    }
    else {
      BKE_fcurve_merge_duplicate_keys(static_cast<FCurve *>(ale->key_data),
                                      SELECT,
                                      false); /* only use handles in graph editor */
    }
  }

  /* free temp data */
  ANIM_animdata_freelist(&anim_data);
}

static void special_aftertrans_update__actedit(bContext *C, TransInfo *t)
{
  SpaceAction *saction = (SpaceAction *)t->area->spacedata.first;
  bAnimContext ac;

  const bool canceled = (t->state == TRANS_CANCEL);
  const bool duplicate = (t->flag & T_AUTOMERGE) != 0;

  /* initialize relevant anim-context 'context' data */
  if (ANIM_animdata_get_context(C, &ac) == 0) {
    return;
  }

  Object *ob = ac.obact;

  if (ELEM(ac.datatype, ANIMCONT_DOPESHEET, ANIMCONT_SHAPEKEY, ANIMCONT_TIMELINE)) {
    ListBase anim_data = {nullptr, nullptr};
    bAnimListElem *ale;
    short filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_FOREDIT);

    /* get channels to work on */
    ANIM_animdata_filter(
        &ac, &anim_data, eAnimFilter_Flags(filter), ac.data, eAnimCont_Types(ac.datatype));

    for (ale = static_cast<bAnimListElem *>(anim_data.first); ale; ale = ale->next) {
      switch (ale->datatype) {
        case ALE_GPFRAME:
          ale->id->tag &= ~LIB_TAG_DOIT;
          posttrans_gpd_clean((bGPdata *)ale->id);
          break;

        case ALE_FCURVE: {
          AnimData *adt = ANIM_nla_mapping_get(&ac, ale);
          FCurve *fcu = (FCurve *)ale->key_data;

          /* 3 cases here for curve cleanups:
           * 1) NOTRANSKEYCULL on    -> cleanup of duplicates shouldn't be done
           * 2) canceled == 0        -> user confirmed the transform,
           *                            so duplicates should be removed
           * 3) canceled + duplicate -> user canceled the transform,
           *                            but we made duplicates, so get rid of these
           */
          if ((saction->flag & SACTION_NOTRANSKEYCULL) == 0 && ((canceled == 0) || (duplicate))) {
            if (adt) {
              ANIM_nla_mapping_apply_fcurve(adt, fcu, 0, 0);
              BKE_fcurve_merge_duplicate_keys(
                  fcu, SELECT, false); /* only use handles in graph editor */
              ANIM_nla_mapping_apply_fcurve(adt, fcu, 1, 0);
            }
            else {
              BKE_fcurve_merge_duplicate_keys(
                  fcu, SELECT, false); /* only use handles in graph editor */
            }
          }
          break;
        }

        default:
          BLI_assert_msg(false, "Keys cannot be transformed into this animation type.");
      }
    }

    /* free temp memory */
    ANIM_animdata_freelist(&anim_data);
  }
  else if (ac.datatype == ANIMCONT_ACTION) { /* TODO: just integrate into the above. */
    /* Depending on the lock status, draw necessary views */
    /* FIXME: some of this stuff is not good. */
    if (ob) {
      if (ob->pose || BKE_key_from_object(ob)) {
        DEG_id_tag_update(&ob->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_ANIMATION);
      }
      else {
        DEG_id_tag_update(&ob->id, ID_RECALC_TRANSFORM);
      }
    }

    /* 3 cases here for curve cleanups:
     * 1) NOTRANSKEYCULL on    -> cleanup of duplicates shouldn't be done
     * 2) canceled == 0        -> user confirmed the transform,
     *                            so duplicates should be removed.
     * 3) canceled + duplicate -> user canceled the transform,
     *                            but we made duplicates, so get rid of these.
     */
    if ((saction->flag & SACTION_NOTRANSKEYCULL) == 0 && ((canceled == 0) || (duplicate))) {
      posttrans_action_clean(&ac, (bAction *)ac.data);
    }
  }
  else if (ac.datatype == ANIMCONT_GPENCIL) {
    /* remove duplicate frames and also make sure points are in order! */
    /* 3 cases here for curve cleanups:
     * 1) NOTRANSKEYCULL on    -> cleanup of duplicates shouldn't be done
     * 2) canceled == 0        -> user confirmed the transform,
     *                            so duplicates should be removed
     * 3) canceled + duplicate -> user canceled the transform,
     *                            but we made duplicates, so get rid of these
     */
    if ((saction->flag & SACTION_NOTRANSKEYCULL) == 0 && ((canceled == 0) || (duplicate))) {
      ListBase anim_data = {nullptr, nullptr};
      const int filter = ANIMFILTER_DATA_VISIBLE;
      ANIM_animdata_filter(
          &ac, &anim_data, eAnimFilter_Flags(filter), ac.data, eAnimCont_Types(ac.datatype));

      LISTBASE_FOREACH (bAnimListElem *, ale, &anim_data) {
        if (ale->datatype == ALE_GPFRAME) {
          ale->id->tag &= ~LIB_TAG_DOIT;
          posttrans_gpd_clean((bGPdata *)ale->id);
        }
      }
      ANIM_animdata_freelist(&anim_data);
    }
  }
  else if (ac.datatype == ANIMCONT_MASK) {
    /* remove duplicate frames and also make sure points are in order! */
    /* 3 cases here for curve cleanups:
     * 1) NOTRANSKEYCULL on:
     *    Cleanup of duplicates shouldn't be done.
     * 2) canceled == 0:
     *    User confirmed the transform, so duplicates should be removed.
     * 3) Canceled + duplicate:
     *    User canceled the transform, but we made duplicates, so get rid of these.
     */
    if ((saction->flag & SACTION_NOTRANSKEYCULL) == 0 && ((canceled == 0) || (duplicate))) {
      ListBase anim_data = {nullptr, nullptr};
      const int filter = ANIMFILTER_DATA_VISIBLE;
      ANIM_animdata_filter(
          &ac, &anim_data, eAnimFilter_Flags(filter), ac.data, eAnimCont_Types(ac.datatype));

      LISTBASE_FOREACH (bAnimListElem *, ale, &anim_data) {
        if (ale->datatype == ALE_MASKLAY) {
          ale->id->tag &= ~LIB_TAG_DOIT;
          posttrans_mask_clean((Mask *)ale->id);
        }
      }
      ANIM_animdata_freelist(&anim_data);
    }
  }

  /* marker transform, not especially nice but we may want to move markers
   * at the same time as keyframes in the dope sheet.
   */
  if ((saction->flag & SACTION_MARKERS_MOVE) && (canceled == 0)) {
    if (t->mode == TFM_TIME_TRANSLATE) {
#if 0
      if (ELEM(t->frame_side, 'L', 'R')) { /* TFM_TIME_EXTEND */
        /* same as below */
        ED_markers_post_apply_transform(
            ED_context_get_markers(C), t->scene, t->mode, t->values_final[0], t->frame_side);
      }
      else /* TFM_TIME_TRANSLATE */
#endif
      {
        ED_markers_post_apply_transform(
            ED_context_get_markers(C), t->scene, t->mode, t->values_final[0], t->frame_side);
      }
    }
    else if (t->mode == TFM_TIME_SCALE) {
      ED_markers_post_apply_transform(
          ED_context_get_markers(C), t->scene, t->mode, t->values_final[0], t->frame_side);
    }
  }

  /* make sure all F-Curves are set correctly */
  if (!ELEM(ac.datatype, ANIMCONT_GPENCIL)) {
    ANIM_editkeyframes_refresh(&ac);
  }

  /* clear flag that was set for time-slide drawing */
  saction->flag &= ~SACTION_MOVING;
}

/** \} */

TransConvertTypeInfo TransConvertType_Action = {
    /*flags*/ (T_POINTS | T_2D_EDIT),
    /*createTransData*/ createTransActionData,
    /*recalcData*/ recalcData_actedit,
    /*special_aftertrans_update*/ special_aftertrans_update__actedit,
};
