#include "rive/shapes/paint/dash_path.hpp"
#include "rive/shapes/paint/dash.hpp"
#include "rive/shapes/paint/stroke.hpp"
#include "rive/math/path_measure.hpp"
#include "rive/factory.hpp"

using namespace rive;

void DashEffectPath::invalidateEffect() { m_path.rewind(); }

void DashEffectPath::createPathMeasure(const RawPath* source)
{
    m_pathMeasure = PathMeasure(source);
}

void PathDasher::invalidateDash() {}

ShapePaintPath* PathDasher::dash(ShapePaintPath* destination,
                                 const RawPath* source,
                                 PathMeasure* pathMeasure,
                                 Dash* offset,
                                 Span<Dash*> dashes)
{
    if (destination->hasRenderPath())
    {
        // Previous result hasn't been invalidated, it's still good.
        return destination;
    }

    destination->rewind();
    return applyDash(destination, source, pathMeasure, offset, dashes);
}
ShapePaintPath* PathDasher::applyDash(ShapePaintPath* destination,
                                      const RawPath* source,
                                      PathMeasure* pathMeasure,
                                      Dash* offset,
                                      Span<Dash*> dashes)
{
    // Make sure dashes have some length.
    bool hasValidDash = false;
    for (auto dash : dashes)
    {
        if (dash->normalizedLength(pathMeasure->length(), false) > 0.0f)
        {
            hasValidDash = true;
            break;
        }
    }
    if (hasValidDash)
    {
        int dashIndex = 0;
        auto rawPath = destination->mutableRawPath();
        float dashed = 0.0f;
        float distance = offset->normalizedLength(pathMeasure->length(), true);
        bool draw = true;
        while (dashed < pathMeasure->length())
        {
            const Dash* dash = dashes[dashIndex++ % dashes.size()];
            float dashLength =
                dash->normalizedLength(pathMeasure->length(), false);
            if (dashLength > pathMeasure->length())
            {
                dashLength = pathMeasure->length();
            }
            float endLength = distance + dashLength;
            if (endLength > pathMeasure->length())
            {
                endLength -= pathMeasure->length();
                if (draw)
                {
                    if (distance < pathMeasure->length())
                    {
                        pathMeasure->getSegment(distance,
                                                pathMeasure->length(),
                                                rawPath,
                                                true);
                        pathMeasure->getSegment(0.0f,
                                                endLength,
                                                rawPath,
                                                !pathMeasure->isClosed());
                    }
                    else
                    {
                        pathMeasure->getSegment(0.0f, endLength, rawPath, true);
                    }
                }

                // Setup next step.
                distance = endLength - dashLength;
            }
            else if (draw)
            {
                pathMeasure->getSegment(distance, endLength, rawPath, true);
            }
            distance += dashLength;
            dashed += dashLength;
            draw = !draw;
        }
    }
    return destination;
}

StatusCode DashPath::onAddedClean(CoreContext* context)
{
    auto effectsContainer = EffectsContainer::from(parent());
    if (!effectsContainer)
    {
        return StatusCode::InvalidObject;
    }
    effectsContainer->addStrokeEffect(this);

    m_dashes.clear();
    for (auto child : children())
    {
        if (child->is<Dash>())
        {
            m_dashes.push_back(child->as<Dash>());
        }
    }
    return StatusCode::Ok;
}

void DashPath::offsetChanged() { invalidateEffectFromLocal(); }
void DashPath::offsetIsPercentageChanged() { invalidateEffectFromLocal(); }

void DashPath::updateEffect(PathProvider* pathProvider,
                            const ShapePaintPath* source,
                            ShapePaintType shapePaintType)
{
    auto effectPathIt = m_effectPaths.find(pathProvider);
    if (effectPathIt != m_effectPaths.end())
    {
        auto dashEffectPath =
            static_cast<DashEffectPath*>(effectPathIt->second);
        auto path = dashEffectPath->path();
        if (path->hasRenderPath())
        {
            return;
        }
        path->rewind(source->isLocal());
        // Dash is not supported on fills so it will use the source as output
        if (shapePaintType == ShapePaintType::fill)
        {
            path->addPath(source);
        }
        else
        {
            Dash dashOffset(offset(), offsetIsPercentage());
            dashEffectPath->createPathMeasure(source->rawPath());
            applyDash(path,
                      source->rawPath(),
                      &dashEffectPath->pathMeasure(),
                      &dashOffset,
                      m_dashes);
        }
    }
}

void DashPath::invalidateDash() { invalidateEffectFromLocal(); }

EffectsContainer* DashPath::parentPaint()
{
    return EffectsContainer::from(parent());
}

EffectPath* DashPath::createEffectPath() { return new DashEffectPath(); }