#include "rive/animation/linear_animation.hpp"
#include "rive/animation/keyed_object.hpp"
#include "rive/animation/keyed_callback_reporter.hpp"
#include "rive/artboard.hpp"
#include "rive/importers/artboard_importer.hpp"
#include "rive/importers/import_stack.hpp"
#include "rive/math/math_types.hpp"
#include <cmath>

using namespace rive;

#ifdef TESTING
int LinearAnimation::deleteCount = 0;
#endif

LinearAnimation::LinearAnimation() {}

LinearAnimation::~LinearAnimation()
{
#ifdef TESTING
    deleteCount++;
#endif
}

StatusCode LinearAnimation::onAddedDirty(CoreContext* context)
{
    StatusCode code;
    for (const auto& object : m_KeyedObjects)
    {
        if ((code = object->onAddedDirty(context)) != StatusCode::Ok)
        {
            return code;
        }
    }
    return StatusCode::Ok;
}

StatusCode LinearAnimation::onAddedClean(CoreContext* context)
{
    StatusCode code;
    for (const auto& object : m_KeyedObjects)
    {
        if ((code = object->onAddedClean(context)) != StatusCode::Ok)
        {
            return code;
        }
    }
    return StatusCode::Ok;
}

void LinearAnimation::addKeyedObject(std::unique_ptr<KeyedObject> object)
{
    m_KeyedObjects.push_back(std::move(object));
}

void LinearAnimation::apply(Artboard* artboard, float time, float mix) const
{
    if (quantize())
    {
        float ffps = (float)fps();
        time = std::floor(time * ffps) / ffps;
    }
    for (const auto& object : m_KeyedObjects)
    {
        object->apply(artboard, time, mix);
    }
}

StatusCode LinearAnimation::import(ImportStack& importStack)
{
    auto artboardImporter =
        importStack.latest<ArtboardImporter>(ArtboardBase::typeKey);
    if (artboardImporter == nullptr)
    {
        return StatusCode::MissingObject;
    }
    artboardImporter->addAnimation(this);
    return Super::import(importStack);
}

float LinearAnimation::startSeconds() const
{
    return (enableWorkArea() ? (float)workStart() : 0.0f) / (float)fps();
}
float LinearAnimation::endSeconds() const
{
    return (float)(enableWorkArea() ? workEnd() : duration()) / (float)fps();
}

float LinearAnimation::startTime() const
{
    return (speed() >= 0) ? startSeconds() : endSeconds();
}
float LinearAnimation::startTime(float multiplier) const
{
    return ((speed() * multiplier) >= 0) ? startSeconds() : endSeconds();
}
float LinearAnimation::endTime() const
{
    return (speed() >= 0) ? endSeconds() : startSeconds();
}
float LinearAnimation::durationSeconds() const
{
    return std::abs(endSeconds() - startSeconds());
}

// Matches Dart modulus:
// https://api.dart.dev/stable/2.19.0/dart-core/double/operator_modulo.html
static float positiveMod(float value, float range)
{
    assert(range > 0.0f);
    return math::positive_mod(value, range);
}

float LinearAnimation::globalToLocalSeconds(float seconds) const
{
    switch (loop())
    {
        case Loop::oneShot:
            return seconds + startTime();
        case Loop::loop:
            return positiveMod(seconds, (durationSeconds())) + startTime();
        case Loop::pingPong:
            float localTime = positiveMod(seconds, (durationSeconds()));
            int direction = ((int)(seconds / (durationSeconds()))) % 2;
            return direction == 0 ? localTime + startTime()
                                  : endTime() - localTime;
    }
    RIVE_UNREACHABLE();
}

void LinearAnimation::reportKeyedCallbacks(KeyedCallbackReporter* reporter,
                                           float secondsFrom,
                                           float secondsTo,
                                           float speedDirection,
                                           bool fromPong) const
{
    float startingTime = startTime(speedDirection);
    bool isAtStartFrame = startingTime == secondsFrom;

    if (!isAtStartFrame || !fromPong)
    {
        for (const auto& object : m_KeyedObjects)
        {
            object->reportKeyedCallbacks(reporter,
                                         secondsFrom,
                                         secondsTo,
                                         isAtStartFrame);
        }
    }
}