#include "rive/constraints/scrolling/scroll_constraint.hpp"
#include "rive/constraints/scrolling/scroll_constraint_proxy.hpp"
#include "rive/constraints/scrolling/scroll_virtualizer.hpp"
#include "rive/constraints/transform_constraint.hpp"
#include "rive/core_context.hpp"
#include "rive/layout/layout_node_provider.hpp"
#include "rive/transform_component.hpp"
#include "rive/virtualizing_component.hpp"
#include "rive/math/mat2d.hpp"

using namespace rive;

ScrollConstraint::~ScrollConstraint()
{
    if (m_virtualizer != nullptr)
    {
        delete m_virtualizer;
        m_virtualizer = nullptr;
    }
    m_layoutChildren.clear();
    delete m_physics;
}

float ScrollConstraint::contentWidth()
{
    if (virtualize() && !mainAxisIsColumn())
    {
        auto contentSize = 0.0f;
        for (auto child : scrollChildren())
        {
            if (child == nullptr)
            {
                continue;
            }
            contentSize += child->layoutBounds().width();
        }
        auto lenOffset = infinite() ? 0 : 1;
        return contentSize + gap().x * (scrollChildren().size() - lenOffset);
    }
    return content()->layoutWidth();
}

float ScrollConstraint::contentHeight()
{
    if (virtualize() && mainAxisIsColumn())
    {
        auto contentSize = 0.0f;
        for (auto child : scrollChildren())
        {
            if (child == nullptr)
            {
                continue;
            }
            contentSize += child->layoutBounds().height();
        }
        auto lenOffset = infinite() ? 0 : 1;
        return contentSize + gap().y * (scrollChildren().size() - lenOffset);
    }
    return content()->layoutHeight();
}

float ScrollConstraint::viewportWidth()
{
    return direction() == DraggableConstraintDirection::vertical
               ? viewport()->layoutWidth()
               : std::max(0.0f,
                          viewport()->layoutWidth() - content()->layoutX());
}
float ScrollConstraint::viewportHeight()
{
    return direction() == DraggableConstraintDirection::horizontal
               ? viewport()->layoutHeight()
               : std::max(0.0f,
                          viewport()->layoutHeight() - content()->layoutY());
}
float ScrollConstraint::visibleWidthRatio()
{
    if (contentWidth() == 0)
    {
        return 1;
    }
    return std::min(1.0f, viewportWidth() / contentWidth());
}
float ScrollConstraint::visibleHeightRatio()
{
    if (contentHeight() == 0)
    {
        return 1;
    }
    return std::min(1.0f, viewportHeight() / contentHeight());
}
float ScrollConstraint::minOffsetX()
{
    if (infinite() && !mainAxisIsColumn())
    {
        return std::numeric_limits<float>::infinity();
    }
    return 0;
}
float ScrollConstraint::minOffsetY()
{
    if (infinite() && mainAxisIsColumn())
    {
        return std::numeric_limits<float>::infinity();
    }
    return 0;
}
float ScrollConstraint::maxOffsetX()
{
    if (infinite() && !mainAxisIsColumn())
    {
        return -std::numeric_limits<float>::infinity();
    }
    return std::min(0.0f,
                    viewportWidth() - contentWidth() -
                        viewport()->paddingRight());
}
float ScrollConstraint::maxOffsetY()
{
    if (infinite() && mainAxisIsColumn())
    {
        return -std::numeric_limits<float>::infinity();
    }
    return std::min(0.0f,
                    viewportHeight() - contentHeight() -
                        viewport()->paddingBottom());
}
float ScrollConstraint::clampedOffsetX()
{
    if (infinite())
    {
        return offsetX();
    }
    if (maxOffsetX() > 0)
    {
        return 0;
    }
    if (m_physics != nullptr && m_physics->enabled())
    {
        return m_physics
            ->clamp(Vec2D(maxOffsetX(), maxOffsetY()),
                    Vec2D(minOffsetX(), minOffsetY()),
                    Vec2D(m_offsetX, m_offsetY))
            .x;
    }
    return math::clamp(m_offsetX, maxOffsetX(), 0);
}
float ScrollConstraint::clampedOffsetY()
{
    if (infinite())
    {
        return offsetY();
    }
    if (maxOffsetY() > 0)
    {
        return 0;
    }
    if (m_physics != nullptr && m_physics->enabled())
    {
        return m_physics
            ->clamp(Vec2D(maxOffsetX(), maxOffsetY()),
                    Vec2D(minOffsetX(), minOffsetY()),
                    Vec2D(m_offsetX, m_offsetY))
            .y;
    }
    return math::clamp(m_offsetY, maxOffsetY(), 0);
}

void ScrollConstraint::offsetX(float value)
{
    if (m_offsetX == value)
    {
        return;
    }
    m_offsetX = value;
    content()->markWorldTransformDirty();
}
void ScrollConstraint::offsetY(float value)
{
    if (m_offsetY == value)
    {
        return;
    }
    m_offsetY = value;
    content()->markWorldTransformDirty();
}

bool ScrollConstraint::mainAxisIsColumn()
{
    return content() != nullptr && content()->mainAxisIsColumn();
}

void ScrollConstraint::constrain(TransformComponent* component)
{
    m_scrollTransform =
        Mat2D::fromTranslate(constrainsHorizontal() ? clampedOffsetX() : 0,
                             constrainsVertical() ? clampedOffsetY() : 0);
    m_childConstraintAppliedCount = 0;
}

void ScrollConstraint::constrainChild(LayoutNodeProvider* child)
{
    auto component = child->transformComponent();
    if (component == nullptr)
    {
        return;
    }
    auto targetTransform =
        Mat2D::multiply(component->worldTransform(), m_scrollTransform);
    TransformConstraint::constrainWorld(component,
                                        component->worldTransform(),
                                        m_componentsA,
                                        targetTransform,
                                        m_componentsB,
                                        strength());
    m_childConstraintAppliedCount += 1;
    constrainVirtualized();
}

void ScrollConstraint::constrainVirtualized(bool force)
{
    if (virtualize() && m_virtualizer != nullptr)
    {
        auto children = scrollChildren();
        if (m_childConstraintAppliedCount < children.size() && !force)
        {
            return;
        }
        auto isColumn = mainAxisIsColumn();
        auto direction = isColumn ? VirtualizedDirection::vertical
                                  : VirtualizedDirection::horizontal;
        auto offset = isColumn ? clampedOffsetY() : clampedOffsetX();
        m_virtualizer->constrain(this, children, offset, direction);
    }
}

void ScrollConstraint::addLayoutChild(LayoutNodeProvider* child)
{
    m_layoutChildren.push_back(child);
}

void ScrollConstraint::dragView(Vec2D delta, float timeStamp)
{
    if (m_physics != nullptr)
    {
        m_physics->accumulate(delta, timeStamp);
    }
    scrollOffsetX(offsetX() + delta.x);
    scrollOffsetY(offsetY() + delta.y);
}

void ScrollConstraint::runPhysics()
{
    m_isDragging = false;
    std::vector<Vec2D> snappingPoints;
    if (snap())
    {
        for (auto child : content()->children())
        {
            auto c = LayoutNodeProvider::from(child);
            if (c != nullptr)
            {
                size_t count = c->numLayoutNodes();
                for (int j = 0; j < count; j++)
                {
                    auto bounds = c->layoutBoundsForNode(j);
                    if (isBoundsCollapsed(bounds))
                    {
                        continue;
                    }
                    snappingPoints.push_back(
                        Vec2D(bounds.left(), bounds.top()));
                }
            }
        }
    }
    if (m_physics != nullptr)
    {
        m_physics->run(Vec2D(maxOffsetX(), maxOffsetY()),
                       Vec2D(minOffsetX(), minOffsetY()),
                       Vec2D(offsetX(), offsetY()),
                       snap() ? snappingPoints : std::vector<Vec2D>(),
                       mainAxisIsColumn() ? contentHeight() : contentWidth());
    }
}

bool ScrollConstraint::advanceComponent(float elapsedSeconds,
                                        AdvanceFlags flags)
{
    if ((flags & AdvanceFlags::AdvanceNested) != AdvanceFlags::AdvanceNested)
    {
        // offsetX(0);
        // offsetY(0);
        return false;
    }
    if (m_physics == nullptr)
    {
        return false;
    }
    if (m_physics->isRunning())
    {
        auto offset = m_physics->advance(elapsedSeconds);
        scrollOffsetX(offset.x);
        scrollOffsetY(offset.y);
    }
    return m_physics->enabled();
}

std::vector<DraggableProxy*> ScrollConstraint::draggables()
{
    std::vector<DraggableProxy*> items;
    items.push_back(new ViewportDraggableProxy(this, viewport()->proxy()));
    return items;
}

void ScrollConstraint::buildDependencies()
{
    Super::buildDependencies();
    for (auto child : content()->children())
    {
        auto layout = LayoutNodeProvider::from(child);
        if (layout != nullptr)
        {
            addDependent(child);
            layout->addLayoutConstraint(static_cast<LayoutConstraint*>(this));
        }
    }
}

Core* ScrollConstraint::clone() const
{
    auto cloned = ScrollConstraintBase::clone();
    if (physics() != nullptr)
    {
        auto constraint = cloned->as<ScrollConstraint>();
        auto clonedPhysics = physics()->clone()->as<ScrollPhysics>();
        constraint->physics(clonedPhysics);
    }
    return cloned;
}

StatusCode ScrollConstraint::import(ImportStack& importStack)
{
    auto backboardImporter =
        importStack.latest<BackboardImporter>(BackboardBase::typeKey);
    if (backboardImporter != nullptr)
    {
        std::vector<ScrollPhysics*> physicsObjects =
            backboardImporter->physics();
        if (physicsId() != -1 && physicsId() < physicsObjects.size())
        {
            auto phys = physicsObjects[physicsId()];
            if (phys != nullptr)
            {
                auto cloned = phys->clone()->as<ScrollPhysics>();
                physics(cloned);
            }
        }
    }
    else
    {
        return StatusCode::MissingObject;
    }
    return Super::import(importStack);
}

StatusCode ScrollConstraint::onAddedDirty(CoreContext* context)
{
    StatusCode result = Super::onAddedDirty(context);
    if (virtualize())
    {
        m_virtualizer = new ScrollVirtualizer();
    }
    offsetX(scrollOffsetX());
    offsetY(scrollOffsetY());
    return result;
}

void ScrollConstraint::initPhysics()
{
    m_isDragging = true;
    if (m_physics != nullptr)
    {
        m_physics->prepare(direction());
    }
}

void ScrollConstraint::stopPhysics()
{
    if (m_physics != nullptr)
    {
        m_physics->reset();
    }
}

float ScrollConstraint::maxOffsetXForPercent()
{
    return infinite() ? contentWidth() : maxOffsetX();
}

float ScrollConstraint::maxOffsetYForPercent()
{
    return infinite() ? contentHeight() : maxOffsetY();
}

float ScrollConstraint::scrollPercentX()
{
    return maxOffsetX() != 0 ? scrollOffsetX() / maxOffsetXForPercent() : 0;
}

float ScrollConstraint::scrollPercentY()
{
    return maxOffsetY() != 0 ? scrollOffsetY() / maxOffsetYForPercent() : 0;
}

float ScrollConstraint::scrollIndex()
{
    return indexAtPosition(Vec2D(scrollOffsetX(), scrollOffsetY()));
}

void ScrollConstraint::setScrollPercentX(float value)
{
    if (m_isDragging)
    {
        return;
    }
    stopPhysics();
    float to = value * maxOffsetXForPercent();
    scrollOffsetX(to);
}

void ScrollConstraint::setScrollPercentY(float value)
{
    if (m_isDragging)
    {
        return;
    }
    stopPhysics();
    float to = value * maxOffsetYForPercent();
    scrollOffsetY(to);
}

void ScrollConstraint::setScrollIndex(float value)
{
    if (m_isDragging)
    {
        return;
    }
    stopPhysics();
    Vec2D to = positionAtIndex(value);
    if (constrainsHorizontal())
    {
        scrollOffsetX(to.x);
    }
    else if (constrainsVertical())
    {
        scrollOffsetY(to.y);
    }
}

Vec2D ScrollConstraint::positionAtIndex(float index)
{
    auto count = scrollItemCount();
    if (content() == nullptr || count == 0)
    {
        return Vec2D();
    }
    uint32_t i = 0;
    Vec2D contentGap = gap();
    float normalizedIndex = infinite() ? std::fmod(index, (float)count) : index;
    float floorIndex = std::floor(normalizedIndex);
    LayoutNodeProvider* lastChild = nullptr;
    for (auto child : scrollChildren())
    {
        if (child == nullptr)
        {
            continue;
        }
        size_t count = child->numLayoutNodes();
        if ((uint32_t)floorIndex < i + count)
        {
            float mod = normalizedIndex - floorIndex;
            auto bounds = child->layoutBoundsForNode(floorIndex - i);
            return Vec2D(-bounds.left() - (bounds.width() + contentGap.x) * mod,
                         -bounds.top() -
                             (bounds.height() + contentGap.y) * mod);
        }
        lastChild = child;
        i += count;
    }
    if (lastChild == nullptr)
    {
        return Vec2D();
    }

    auto bounds =
        lastChild->layoutBoundsForNode((int)lastChild->numLayoutNodes() - 1);
    return Vec2D(-bounds.left(), -bounds.top());
}

float ScrollConstraint::indexAtPosition(Vec2D pos)
{
    if (content() == nullptr || content()->children().size() == 0)
    {
        return 0;
    }
    float i = 0.0f;
    Vec2D contentGap = gap();
    if (constrainsHorizontal())
    {
        for (auto child : scrollChildren())
        {
            if (child == nullptr)
            {
                continue;
            }
            size_t count = child->numLayoutNodes();
            for (int j = 0; j < count; j++)
            {
                auto bounds = child->layoutBoundsForNode(j);
                if (pos.x > -bounds.left() - (bounds.width() + contentGap.x))
                {
                    return (i + j) + (-pos.x - bounds.left()) /
                                         (bounds.width() + contentGap.x);
                }
            }
            i += count;
        }
        return i;
    }
    else if (constrainsVertical())
    {
        for (auto child : scrollChildren())
        {
            if (child == nullptr)
            {
                continue;
            }
            size_t count = child->numLayoutNodes();
            for (int j = 0; j < count; j++)
            {
                auto bounds = child->layoutBoundsForNode(j);
                if (pos.y > -bounds.top() - (bounds.height() + contentGap.y))
                {
                    return (i + j) + (-pos.y - bounds.top()) /
                                         (bounds.height() + contentGap.y);
                }
            }
            i += count;
        }
        return i;
    }
    return 0;
}

bool ScrollConstraint::isBoundsCollapsed(AABB bounds)
{
    return (constrainsHorizontal() && bounds.width() <= 0) ||
           (constrainsVertical() && bounds.height() <= 0);
}

size_t ScrollConstraint::scrollItemCount()
{
    size_t count = 0;
    for (auto child : scrollChildren())
    {
        if (child == nullptr)
        {
            continue;
        }
        count += child->numLayoutNodes();
    }
    return count;
}

Vec2D ScrollConstraint::gap()
{
    if (content() == nullptr)
    {
        return Vec2D();
    }
    return Vec2D(content()->gapHorizontal(), content()->gapVertical());
}