/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * The Original Code is Copyright (C) 2021 Blender Foundation
 * All rights reserved.
 */

#include "BKE_action.h"

#include "DNA_action_types.h"
#include "DNA_anim_types.h"

#include "BLI_listbase.h"

#include "MEM_guardedalloc.h"

#include "testing/testing.h"

namespace blender::bke::tests {

TEST(action_groups, ReconstructGroupsWithReordering)
{
  /* Construct an Action with three groups. */
  bAction action = {{nullptr}};
  FCurve groupAcurve1 = {nullptr};
  FCurve groupAcurve2 = {nullptr};
  FCurve groupBcurve1 = {nullptr};
  FCurve groupBcurve2 = {nullptr};
  FCurve groupBcurve3 = {nullptr};
  /* Group C has no curves intentionally. */
  FCurve groupDcurve1 = {nullptr};
  FCurve groupDcurve2 = {nullptr};

  groupAcurve1.rna_path = (char *)"groupAcurve1";
  groupAcurve2.rna_path = (char *)"groupAcurve2";
  groupBcurve1.rna_path = (char *)"groupBcurve1";
  groupBcurve2.rna_path = (char *)"groupBcurve2";
  groupDcurve1.rna_path = (char *)"groupDcurve1";
  groupBcurve3.rna_path = (char *)"groupBcurve3";
  groupDcurve2.rna_path = (char *)"groupDcurve2";

  BLI_addtail(&action.curves, &groupAcurve1);
  BLI_addtail(&action.curves, &groupAcurve2);
  BLI_addtail(&action.curves, &groupBcurve1);
  BLI_addtail(&action.curves, &groupBcurve2);
  BLI_addtail(&action.curves, &groupDcurve1);
  BLI_addtail(&action.curves, &groupBcurve3); /* <-- The error that should be corrected. */
  BLI_addtail(&action.curves, &groupDcurve2);

  /* Introduce another error type, by changing some `prev` pointers. */
  groupBcurve1.prev = nullptr;
  groupBcurve3.prev = &groupBcurve2;
  groupDcurve1.prev = &groupBcurve3;

  bActionGroup groupA = {nullptr};
  bActionGroup groupB = {nullptr};
  bActionGroup groupC = {nullptr};
  bActionGroup groupD = {nullptr};
  strcpy(groupA.name, "groupA");
  strcpy(groupB.name, "groupB");
  strcpy(groupC.name, "groupC");
  strcpy(groupD.name, "groupD");

  BLI_addtail(&action.groups, &groupA);
  BLI_addtail(&action.groups, &groupB);
  BLI_addtail(&action.groups, &groupC);
  BLI_addtail(&action.groups, &groupD);

  groupAcurve1.grp = &groupA;
  groupAcurve2.grp = &groupA;
  groupBcurve1.grp = &groupB;
  groupBcurve2.grp = &groupB;
  groupBcurve3.grp = &groupB;
  groupDcurve1.grp = &groupD;
  groupDcurve2.grp = &groupD;

  groupA.channels.first = &groupAcurve1;
  groupA.channels.last = &groupAcurve2;
  groupB.channels.first = &groupBcurve1;
  groupB.channels.last = &groupBcurve3; /* The last channel in group B, after group C curve 1. */
  groupD.channels.first = &groupDcurve1;
  groupD.channels.last = &groupDcurve2;

  EXPECT_EQ(groupA.channels.first, &groupAcurve1);
  EXPECT_EQ(groupA.channels.last, &groupAcurve2);
  EXPECT_EQ(groupB.channels.first, &groupBcurve1);
  EXPECT_EQ(groupB.channels.last, &groupBcurve3);
  EXPECT_EQ(groupC.channels.first, nullptr);
  EXPECT_EQ(groupC.channels.last, nullptr);
  EXPECT_EQ(groupD.channels.first, &groupDcurve1);
  EXPECT_EQ(groupD.channels.last, &groupDcurve2);

  BKE_action_groups_reconstruct(&action);

  EXPECT_EQ(action.curves.first, &groupAcurve1);
  EXPECT_EQ(action.curves.last, &groupDcurve2);

  EXPECT_EQ(groupA.prev, nullptr);
  EXPECT_EQ(groupB.prev, &groupA);
  EXPECT_EQ(groupC.prev, &groupB);
  EXPECT_EQ(groupD.prev, &groupC);

  EXPECT_EQ(groupA.next, &groupB);
  EXPECT_EQ(groupB.next, &groupC);
  EXPECT_EQ(groupC.next, &groupD);
  EXPECT_EQ(groupD.next, nullptr);

  EXPECT_EQ(groupA.channels.first, &groupAcurve1);
  EXPECT_EQ(groupA.channels.last, &groupAcurve2);
  EXPECT_EQ(groupB.channels.first, &groupBcurve1);
  EXPECT_EQ(groupB.channels.last, &groupBcurve3);
  EXPECT_EQ(groupC.channels.first, nullptr);
  EXPECT_EQ(groupC.channels.last, nullptr);
  EXPECT_EQ(groupD.channels.first, &groupDcurve1);
  EXPECT_EQ(groupD.channels.last, &groupDcurve2);

  EXPECT_EQ(groupAcurve1.prev, nullptr);
  EXPECT_EQ(groupAcurve2.prev, &groupAcurve1);
  EXPECT_EQ(groupBcurve1.prev, &groupAcurve2);
  EXPECT_EQ(groupBcurve2.prev, &groupBcurve1);
  EXPECT_EQ(groupBcurve3.prev, &groupBcurve2);
  EXPECT_EQ(groupDcurve1.prev, &groupBcurve3);
  EXPECT_EQ(groupDcurve2.prev, &groupDcurve1);

  EXPECT_EQ(groupAcurve1.next, &groupAcurve2);
  EXPECT_EQ(groupAcurve2.next, &groupBcurve1);
  EXPECT_EQ(groupBcurve1.next, &groupBcurve2);
  EXPECT_EQ(groupBcurve2.next, &groupBcurve3);
  EXPECT_EQ(groupBcurve3.next, &groupDcurve1);
  EXPECT_EQ(groupDcurve1.next, &groupDcurve2);
  EXPECT_EQ(groupDcurve2.next, nullptr);
}

namespace {

/* Allocate fcu->bezt, and also return a unique_ptr to it for easily freeing the memory. */
std::unique_ptr<BezTriple[]> allocate_keyframes(FCurve *fcu, const size_t num_keyframes)
{
  auto bezt_uptr = std::make_unique<BezTriple[]>(num_keyframes);
  fcu->bezt = bezt_uptr.get();
  return bezt_uptr;
}

/* Append keyframe, assumes that fcu->bezt is allocated and has enough space. */
void add_keyframe(FCurve *fcu, float x, float y)
{
  /* The insert_keyframe functions are in the editors, so we cannot link to those here. */
  BezTriple the_keyframe;
  memset(&the_keyframe, 0, sizeof(the_keyframe));

  /* Copied from insert_vert_fcurve() in keyframing.c. */
  the_keyframe.vec[0][0] = x - 1.0f;
  the_keyframe.vec[0][1] = y;
  the_keyframe.vec[1][0] = x;
  the_keyframe.vec[1][1] = y;
  the_keyframe.vec[2][0] = x + 1.0f;
  the_keyframe.vec[2][1] = y;

  memcpy(&fcu->bezt[fcu->totvert], &the_keyframe, sizeof(the_keyframe));
  fcu->totvert++;
}

}  // namespace

TEST(action_assets, BKE_action_has_single_frame)
{
  /* NULL action. */
  EXPECT_FALSE(BKE_action_has_single_frame(nullptr)) << "NULL Action cannot have a single frame.";

  /* No FCurves. */
  {
    const bAction empty = {{nullptr}};
    EXPECT_FALSE(BKE_action_has_single_frame(&empty))
        << "Action without FCurves cannot have a single frame.";
  }

  /* One curve with one key. */
  {
    FCurve fcu = {nullptr};
    std::unique_ptr<BezTriple[]> bezt = allocate_keyframes(&fcu, 1);
    add_keyframe(&fcu, 1.0f, 2.0f);

    bAction action = {{nullptr}};
    BLI_addtail(&action.curves, &fcu);

    EXPECT_TRUE(BKE_action_has_single_frame(&action))
        << "Action with one FCurve and one key should have single frame.";
  }

  /* Two curves with one key each. */
  {
    FCurve fcu1 = {nullptr};
    FCurve fcu2 = {nullptr};
    std::unique_ptr<BezTriple[]> bezt1 = allocate_keyframes(&fcu1, 1);
    std::unique_ptr<BezTriple[]> bezt2 = allocate_keyframes(&fcu2, 1);
    add_keyframe(&fcu1, 1.0f, 327.0f);
    add_keyframe(&fcu2, 1.0f, 47.0f); /* Same X-coordinate as the other one. */

    bAction action = {{nullptr}};
    BLI_addtail(&action.curves, &fcu1);
    BLI_addtail(&action.curves, &fcu2);

    EXPECT_TRUE(BKE_action_has_single_frame(&action))
        << "Two FCurves with keys on the same frame should have single frame.";

    /* Modify the 2nd curve so it's keyed on a different frame. */
    fcu2.bezt[0].vec[1][0] = 2.0f;
    EXPECT_FALSE(BKE_action_has_single_frame(&action))
        << "Two FCurves with keys on different frames should have animation.";
  }

  /* One curve with two keys. */
  {
    FCurve fcu = {nullptr};
    std::unique_ptr<BezTriple[]> bezt = allocate_keyframes(&fcu, 2);
    add_keyframe(&fcu, 1.0f, 2.0f);
    add_keyframe(&fcu, 2.0f, 2.5f);

    bAction action = {{nullptr}};
    BLI_addtail(&action.curves, &fcu);

    EXPECT_FALSE(BKE_action_has_single_frame(&action))
        << "Action with one FCurve and two keys must have animation.";
  }
}

}  // namespace blender::bke::tests
