#include "rive/shapes/paint/trim_path.hpp"
#include "rive/shapes/paint/shape_paint.hpp"
#include "rive/profiler/profiler_macros.h"
using namespace rive;

StatusCode TrimPath::onAddedClean(CoreContext* context)
{
    if (!parent()->is<ShapePaint>())
    {
        return StatusCode::InvalidObject;
    }

    parent()->as<ShapePaint>()->addStrokeEffect(this);

    return StatusCode::Ok;
}

void TrimPath::trimPath(const RawPath* source, ShapePaintType shapePaintType)
{
    RIVE_PROF_SCOPE()
    auto rawPath = m_path.mutableRawPath();
    auto renderOffset = std::fmod(std::fmod(offset(), 1.0f) + 1.0f, 1.0f);
    auto closeShape = shapePaintType == ShapePaintType::fill;

    // Build up contours if they're empty.
    if (m_contours.empty())
    {
        ContourMeasureIter iter(source);
        while (auto meas = iter.next())
        {
            m_contours.push_back(meas);
        }
    }
    switch (mode())
    {
        case TrimPathMode::sequential:
        {
            float totalLength = 0.0f;
            for (auto contour : m_contours)
            {
                totalLength += contour->length();
            }
            auto startLength = totalLength * (start() + renderOffset);
            auto endLength = totalLength * (end() + renderOffset);

            if (endLength < startLength)
            {
                float swap = startLength;
                startLength = endLength;
                endLength = swap;
            }

            if (startLength > totalLength)
            {
                startLength -= totalLength;
                endLength -= totalLength;
            }

            int i = 0, subPathCount = (int)m_contours.size();
            std::vector<int> indices;
            std::vector<float> lengths;
            while (endLength > 0)
            {
                auto currentContourIndex = i % subPathCount;
                auto contour = m_contours[currentContourIndex];
                auto contourLength = contour->length();

                if (startLength < contourLength)
                {
                    indices.push_back(currentContourIndex);
                    lengths.push_back(startLength);
                    lengths.push_back(endLength);
                    endLength -= contourLength;
                    startLength = 0;
                }
                else
                {
                    startLength -= contourLength;
                    endLength -= contourLength;
                }
                i++;
            }

            // This is inintuitive but works. If the last segment and the first
            // segment of the trim path belong to the same contour, we want to
            // draw the first semgment first, then the last one and then the
            // rest. This is intentional to make sure the last segment is
            // connected to the first one in order to avoid a gap. A linear way
            // without reordering indices is to start at index 0 and go
            // backwards. For the remaining segments it's not important in what
            // order they are drawn
            int startingIndex = 0;
            int indexCount = 0;

            int prevContourIndex = -1;
            while (indexCount < indices.size())
            {
                auto index = (startingIndex < 0 ? startingIndex + indices.size()
                                                : startingIndex) %
                             indices.size();
                auto contourIndex = indices[index];
                auto contour = m_contours[contourIndex];
                auto contourLength = contour->length();
                auto lengthIndex = index * 2;
                auto startLength = lengths[lengthIndex];
                auto endLength = lengths[lengthIndex + 1];
                // if two consecutive segments belong to the same contour, draw
                // them connected.
                contour->getSegment(startLength,
                                    endLength,
                                    rawPath,
                                    prevContourIndex != contourIndex ||
                                        !contour->isClosed());
                // Close contours that are fully used as
                // segments
                if ((startLength == 0.0f &&
                     endLength - startLength >= contourLength &&
                     contour->isClosed()) ||
                    closeShape)
                {
                    rawPath->close();
                }
                prevContourIndex = contourIndex;
                indexCount++;
                startingIndex--;
            }
        }
        break;

        case TrimPathMode::synchronized:
        {
            for (auto contour : m_contours)
            {
                auto contourLength = contour->length();
                auto startLength = contourLength * (start() + renderOffset);
                auto endLength = contourLength * (end() + renderOffset);
                if (endLength < startLength)
                {
                    auto length = startLength;
                    startLength = endLength;
                    endLength = length;
                }

                if (startLength >= contourLength)
                {
                    startLength -= contourLength;
                    endLength -= contourLength;
                }
                contour->getSegment(startLength, endLength, rawPath, true);
                while (endLength > contourLength)
                {
                    startLength = 0;
                    endLength -= contourLength;
                    contour->getSegment(startLength,
                                        endLength,
                                        rawPath,
                                        !contour->isClosed());
                }

                if ((start() == 0.0f && end() == 1.0f && contour->isClosed()) ||
                    closeShape)
                {
                    rawPath->close();
                }
            }
        }
        break;
        default:
            RIVE_UNREACHABLE();
    }
}

void TrimPath::invalidateEffect()
{
    StrokeEffect::invalidateEffect();
    m_path.rewind();
    // This is usually sent when the path is changed so we need to also
    // invalidate the contours, not just the trim effect.
    m_contours.clear();
}

void TrimPath::startChanged() { invalidateEffectFromLocal(); }
void TrimPath::endChanged() { invalidateEffectFromLocal(); }
void TrimPath::offsetChanged() { invalidateEffectFromLocal(); }
void TrimPath::modeValueChanged() { invalidateEffectFromLocal(); }

StatusCode TrimPath::onAddedDirty(CoreContext* context)
{
    auto code = Super::onAddedDirty(context);
    if (code != StatusCode::Ok)
    {
        return code;
    }
    switch (mode())
    {
        case TrimPathMode::sequential:
        case TrimPathMode::synchronized:
            return StatusCode::Ok;
    }
    return StatusCode::InvalidObject;
}

void TrimPath::updateEffect(const ShapePaintPath* source,
                            ShapePaintType shapePaintType)
{
    if (m_path.hasRenderPath())
    {
        // Previous result hasn't been invalidated, it's still good.
        return;
    }
    m_path.rewind(source->isLocal(), source->fillRule());
    trimPath(source->rawPath(), shapePaintType);
}

ShapePaintPath* TrimPath::effectPath() { return &m_path; }