/*
 * Copyright 2013 Google Inc.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */
#include <vector>

#include "include/core/SkCanvas.h"
#include "include/core/SkPath.h"
#include "include/effects/SkGradientShader.h"
#include "src/base/SkRandom.h"
#include "tools/Resources.h"
#include "tools/gpu/YUVUtils.h"
#include "tools/viewer/Slide.h"

// Implementation in C++ of some WebKit MotionMark tests
// Tests implemented so far:
// * Canvas Lines
// * Canvas Arcs
// * Paths
// Based on https://github.com/WebKit/MotionMark/blob/main/MotionMark/

class MMObject {
public:
    virtual ~MMObject() = default;

    virtual void draw(SkCanvas* canvas) = 0;

    virtual void animate(double /*nanos*/) = 0;
};

class Stage {
public:
    Stage(SkSize size, int startingObjectCount, int objectIncrement)
            : fSize(size)
            , fStartingObjectCount(startingObjectCount)
            , fObjectIncrement(objectIncrement) {}
    virtual ~Stage() = default;

    // The default impls of draw() and animate() simply iterate over fObjects and call the
    // MMObject function.
    virtual void draw(SkCanvas* canvas) {
        for (size_t i = 0; i < fObjects.size(); ++i) {
            fObjects[i]->draw(canvas);
        }
    }

    virtual bool animate(double nanos) {
        for (size_t i = 0; i < fObjects.size(); ++i) {
            fObjects[i]->animate(nanos);
        }
        return true;
    }

    // The default impl handles +/- to add or remove N objects from the scene
    virtual bool onChar(SkUnichar uni) {
        bool handled = false;
        switch (uni) {
            case '+':
            case '=':
                for (int i = 0; i < fObjectIncrement; ++i) {
                    fObjects.push_back(this->createObject());
                }
                handled = true;
                break;
            case '-':
            case '_':
                if (fObjects.size() > (size_t) fObjectIncrement) {
                    fObjects.resize(fObjects.size() - (size_t) fObjectIncrement);
                }
                handled = true;
                break;
            default:
                break;
        }

        return handled;
    }

protected:
    virtual std::unique_ptr<MMObject> createObject() = 0;

    void initializeObjects() {
        for (int i = 0; i < fStartingObjectCount; ++i) {
            fObjects.push_back(this->createObject());
        }
    }

    [[maybe_unused]] SkSize fSize;

    int fStartingObjectCount;
    int fObjectIncrement;

    std::vector<std::unique_ptr<MMObject>> fObjects;
    SkRandom fRandom;
};

class MotionMarkSlide : public Slide {
public:
    MotionMarkSlide() = default;

    bool onChar(SkUnichar uni) override {
        return fStage->onChar(uni);
    }

    void draw(SkCanvas* canvas) override {
        fStage->draw(canvas);
    }

    bool animate(double nanos) override {
        return fStage->animate(nanos);
    }

protected:
    std::unique_ptr<Stage> fStage;
};


namespace {

float time_counter_value(double nanos, float factor) {
    constexpr double kMillisPerNano = 0.0000001;
    return static_cast<float>(nanos*kMillisPerNano)/factor;
}

float time_fractional_value(double nanos, float cycleLengthMs) {
    return SkScalarFraction(time_counter_value(nanos, cycleLengthMs));
}

// The following functions match the input processing that Chrome's canvas2d layer performs before
// calling into Skia.

// See https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/canvas/canvas2d/canvas_path.cc;drc=572074cb06425797e7e110511db405134cf67e2f;l=299
void canonicalize_angle(float* startAngle, float* endAngle) {
    float newStartAngle = SkScalarMod(*startAngle, 360.f);
    float delta = newStartAngle - *startAngle;
    *startAngle = newStartAngle;
    *endAngle = *endAngle + delta;
}

// See https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/canvas/canvas2d/canvas_path.cc;drc=572074cb06425797e7e110511db405134cf67e2f;l=245
float adjust_end_angle(float startAngle, float endAngle, bool ccw) {
    float newEndAngle = endAngle;
    if (!ccw && endAngle - startAngle >= 360.f) {
        newEndAngle = startAngle + 360.f;
    } else if (ccw && startAngle - endAngle >= 360.f) {
        newEndAngle = startAngle - 360.f;
    } else if (!ccw && startAngle > endAngle) {
        newEndAngle = startAngle + (360.f - SkScalarMod(startAngle - endAngle, 360.f));
    } else if (ccw && startAngle < endAngle) {
        newEndAngle = startAngle - (360.f - SkScalarMod(endAngle - startAngle, 360.f));
    }

    return newEndAngle;
}

}  // namespace

///////////////////////////////////////////////////////////////////////////////////////////////////
// Canvas Lines
///////////////////////////////////////////////////////////////////////////////////////////////////
struct LineSegmentParams {
    float fCircleRadius;
    SkPoint fCircleCenters[4];
    float fLineLengthMaximum;
    float fLineMinimum;
};

class CanvasLineSegment : public MMObject {
public:
    CanvasLineSegment(SkRandom* random, const LineSegmentParams& params) {
        int circle = random->nextRangeU(0, 3);

        static constexpr SkColor kColors[] = {
            0xffe01040, 0xff10c030, 0xff744cba, 0xffe05010
        };
        fColor = kColors[circle];
        fLineWidth = std::pow(random->nextF(), 12) * 20 + 3;
        fOmega = random->nextF() * 3 + 0.2f;
        float theta = random->nextRangeF(0, 2*SK_ScalarPI);
        fCosTheta = std::cos(theta);
        fSinTheta = std::sin(theta);
        fStart = params.fCircleCenters[circle] + SkPoint::Make(params.fCircleRadius * fCosTheta,
                                                               params.fCircleRadius * fSinTheta);
        fLength = params.fLineMinimum;
        fLength += std::pow(random->nextF(), 8) * params.fLineLengthMaximum;
        fSegmentDirection = random->nextF() > 0.5 ? -1 : 1;
    }

    ~CanvasLineSegment() override = default;

    void draw(SkCanvas* canvas) override {
        SkPaint paint;
        paint.setAntiAlias(true);
        paint.setColor(fColor);
        paint.setStrokeWidth(fLineWidth);
        paint.setStyle(SkPaint::kStroke_Style);

        SkPoint end = {
            fStart.fX + fSegmentDirection * fLength * fCosTheta,
            fStart.fY + fSegmentDirection * fLength * fSinTheta
        };
        canvas->drawLine(fStart, end, paint);
    }

    void animate(double nanos) override {
        fLength += std::sin(time_counter_value(nanos, 100) * fOmega);
    }

private:
    SkColor fColor;
    float fLineWidth;
    float fOmega;
    float fCosTheta;
    float fSinTheta;
    SkPoint fStart;
    float fLength;
    float fSegmentDirection;
};

class CanvasLineSegmentStage : public Stage {
public:
    CanvasLineSegmentStage(SkSize size)
            : Stage(size, /*startingObjectCount=*/5000, /*objectIncrement*/1000) {
        fParams.fLineMinimum = 20;
        fParams.fLineLengthMaximum = 40;
        fParams.fCircleRadius = fSize.fWidth/8 - .4 * (fParams.fLineMinimum +
                                                       fParams.fLineLengthMaximum);
        fParams.fCircleCenters[0] = SkPoint::Make(5.5 / 32 * fSize.fWidth, 2.1 / 3*fSize.fHeight);
        fParams.fCircleCenters[1] = SkPoint::Make(12.5 / 32 * fSize.fWidth, .9 / 3*fSize.fHeight);
        fParams.fCircleCenters[2] = SkPoint::Make(19.5 / 32 * fSize.fWidth, 2.1 / 3*fSize.fHeight);
        fParams.fCircleCenters[3] = SkPoint::Make(26.5 / 32 * fSize.fWidth, .9 / 3*fSize.fHeight);
        fHalfSize = SkSize::Make(fSize.fWidth * 0.5f, fSize.fHeight * 0.5f);
        fTwoFifthsSizeX = fSize.fWidth * .4;

        this->initializeObjects();
    }

    ~CanvasLineSegmentStage() override = default;

    void draw(SkCanvas* canvas) override {
        canvas->clear(SK_ColorWHITE);

        float dx = fTwoFifthsSizeX * std::cos(fCurrentAngle);
        float dy = fTwoFifthsSizeX * std::sin(fCurrentAngle);

        float colorStopStep = SkScalarInterp(-.1f, .1f, fCurrentGradientStep);
        int brightnessStep = SkScalarRoundToInt(SkScalarInterp(32, 64, fCurrentGradientStep));

        SkColor color1Step = SkColorSetARGB(brightnessStep,
                                            brightnessStep,
                                            (brightnessStep << 1),
                                            102);
        SkColor color2Step = SkColorSetARGB((brightnessStep << 1),
                                            (brightnessStep << 1),
                                            brightnessStep,
                                            102);
        SkPoint pts[2] = {
            {fHalfSize.fWidth + dx, fHalfSize.fHeight + dy},
            {fHalfSize.fWidth - dx, fHalfSize.fHeight - dy}
        };
        SkColor colors[] = {
            color1Step,
            color1Step,
            color2Step,
            color2Step
        };
        float pos[] = {
            0,
            0.2f + colorStopStep,
            0.8f - colorStopStep,
            1
        };
        sk_sp<SkShader> gradientShader = SkGradientShader::MakeLinear(pts, colors, pos, 4,
                                                                      SkTileMode::kClamp, 0);

        SkPaint paint;
        paint.setAntiAlias(true);
        paint.setStrokeWidth(15);
        for (int i = 0; i < 4; i++) {
            const SkColor strokeColors[] = {
                0xffe01040, 0xff10c030, 0xff744cba, 0xffe05010
            };
            const SkColor fillColors[] = {
                0xff70051d, 0xff016112, 0xff2F0C6E, 0xff702701
            };
            paint.setColor(strokeColors[i]);
            paint.setStyle(SkPaint::kStroke_Style);
            SkRect arcRect = SkRect::MakeXYWH(fParams.fCircleCenters[i].fX - fParams.fCircleRadius,
                                              fParams.fCircleCenters[i].fY- fParams.fCircleRadius,
                                              2*fParams.fCircleRadius,
                                              2*fParams.fCircleRadius);
            canvas->drawArc(arcRect, 0, 360, false, paint);
            paint.setColor(fillColors[i]);
            paint.setStyle(SkPaint::kFill_Style);
            canvas->drawArc(arcRect, 0, 360, false, paint);
            paint.setShader(gradientShader);
            canvas->drawArc(arcRect, 0, 360, false, paint);
            paint.setShader(nullptr);
        }

        this->Stage::draw(canvas);
    }

    bool animate(double nanos) override {
        fCurrentAngle = time_fractional_value(nanos, 3000) * SK_ScalarPI * 2;
        fCurrentGradientStep = 0.5f + 0.5f * std::sin(
                                       time_fractional_value(nanos, 5000) * SK_ScalarPI * 2);

        this->Stage::animate(nanos);
        return true;
    }

    std::unique_ptr<MMObject> createObject() override {
        return std::make_unique<CanvasLineSegment>(&fRandom,fParams);
    }
private:
    LineSegmentParams fParams;
    SkSize fHalfSize;
    float fTwoFifthsSizeX;
    float fCurrentAngle = 0;
    float fCurrentGradientStep = 0.5f;
};

///////////////////////////////////////////////////////////////////////////////////////////////////
// Canvas Arcs
///////////////////////////////////////////////////////////////////////////////////////////////////

class CanvasArc : public MMObject {
public:
    CanvasArc(SkRandom* random, SkSize size) {
        constexpr float kMaxX = 6;
        constexpr float kMaxY = 3;

        const SkColor baseColors[3] = {
            0xff101010, 0xff808080, 0xffc0c0c0
        };
        const SkColor bonusColors[3] = {
            0xffe01040, 0xff10c030, 0xffe05010
        };
        float distanceX = size.fWidth / kMaxX;
        float distanceY = size.fHeight / (kMaxY + 1);
        int randY = random->nextRangeU(0, kMaxY);
        int randX = random->nextRangeU(0, kMaxX - 1 * (randY % 2));

        fPoint = SkPoint::Make(distanceX * (randX + (randY % 2) / 2), distanceY * (randY + 0.5f));

        fRadius = 20 + std::pow(random->nextF(), 5) * (std::min(distanceX, distanceY) / 1.8f);
        fStartAngle = random->nextRangeF(0, 2*SK_ScalarPI);
        fEndAngle = random->nextRangeF(0, 2*SK_ScalarPI);
        fOmega = (random->nextF() - 0.5f) * 0.3f;
        fCounterclockwise = random->nextBool();
        // The MotionMark code appends a random element from an array and appends it to the color
        // array, then randomly picks from that. We'll just pick that random element and use it
        // if the index is out of bounds for the base color array.
        SkColor bonusColor = bonusColors[(randX + sk_float_ceil2int(randY * 0.5f)) % 3];
        int colorIndex = random->nextRangeU(0, 3);
        fColor = colorIndex == 3 ? bonusColor : baseColors[colorIndex];
        fLineWidth = 1 + std::pow(random->nextF(), 5) * 30;
        fDoStroke = random->nextRangeU(0, 3) != 0;
    }

    ~CanvasArc() override = default;

    void draw(SkCanvas* canvas) override {
        SkPaint paint;
        paint.setAntiAlias(true);
        paint.setColor(fColor);
        SkRect arcRect = SkRect::MakeXYWH(fPoint.fX - fRadius, fPoint.fY - fRadius,
                                          2*fRadius, 2*fRadius);

        float startAngleDeg = fStartAngle * 180.f / SK_ScalarPI;
        float endAngleDeg = fEndAngle * 180.f / SK_ScalarPI;
        canonicalize_angle(&startAngleDeg, &endAngleDeg);
        endAngleDeg = adjust_end_angle(startAngleDeg, endAngleDeg, fCounterclockwise);

        float sweepAngle = startAngleDeg - endAngleDeg;

        if (fDoStroke) {
            paint.setStrokeWidth(fLineWidth);
            paint.setStyle(SkPaint::kStroke_Style);
            canvas->drawArc(arcRect, startAngleDeg, sweepAngle, false, paint);
        } else {
            paint.setStyle(SkPaint::kFill_Style);
            // The MotionMark code creates a path for fills via lineTo(point), arc(), lineTo(point).
            // For now we'll just use drawArc for both but might need to revisit.
            canvas->drawArc(arcRect, startAngleDeg, sweepAngle, true, paint);
        }
    }

    void animate(double /*nanos*/) override {
        fStartAngle += fOmega;
        fEndAngle += fOmega / 2;
    }

private:
    SkPoint fPoint;
    float fRadius;
    float fStartAngle; // in radians
    float fEndAngle;   // in radians
    SkColor fColor;
    float fOmega;      // in radians
    bool fDoStroke;
    bool fCounterclockwise;
    float fLineWidth;
};

class CanvasArcStage : public Stage {
public:
    CanvasArcStage(SkSize size)
            : Stage(size, /*startingObjectCount=*/1000, /*objectIncrement=*/200) {
        this->initializeObjects();
    }

    ~CanvasArcStage() override = default;

    void draw(SkCanvas* canvas) override {
        canvas->clear(SK_ColorWHITE);
        this->Stage::draw(canvas);
    }

    std::unique_ptr<MMObject> createObject() override {
        return std::make_unique<CanvasArc>(&fRandom, fSize);
    }
};

///////////////////////////////////////////////////////////////////////////////////////////////////
// Paths
///////////////////////////////////////////////////////////////////////////////////////////////////

class CanvasLinePoint : public MMObject {
protected:
    void setEndPoint(SkRandom* random, SkSize size, SkPoint* prevCoord) {
        const SkSize kGridSize = { 80, 40 };
        const SkPoint kGridCenter = { 40, 20 };
        const SkPoint kOffsets[4] = {
            {-4, 0},
            {2, 0},
            {1, -2},
            {1, 2}
        };

        SkPoint coordinate = prevCoord ? *prevCoord : kGridCenter;
        if (prevCoord) {
            SkPoint offset = kOffsets[random->nextRangeU(0, 3)];
            coordinate += offset;
            if (coordinate.fX < 0 || coordinate.fX > kGridSize.width())
                coordinate.fX -= offset.fX * 2;
            if (coordinate.fY < 0 || coordinate.fY > kGridSize.height())
                coordinate.fY -= offset.fY * 2;
        }

        fPoint = SkPoint::Make((coordinate.fX + 0.5f) * size.width() / (kGridSize.width() + 1),
                               (coordinate.fY + 0.5f) * size.height() / (kGridSize.height() + 1));
        fCoordinate = coordinate;
    }

public:
    CanvasLinePoint(SkRandom* random, SkSize size, SkPoint* prev) {
        const SkColor kColors[7] = {
            0xff101010, 0xff808080, 0xffc0c0c0, 0xff101010, 0xff808080, 0xffc0c0c0, 0xffe01040
        };
        fColor = kColors[random->nextRangeU(0, 6)];

        fWidth = std::pow(random->nextF(), 5) * 20 + 1;
        fIsSplit = random->nextBool();

        this->setEndPoint(random, size, prev);
    }

    ~CanvasLinePoint() override = default;

    virtual void append(SkPath* path) {
        path->lineTo(fPoint);
    }

    // unused, all the work is done by append
    void draw(SkCanvas*) override {}
    void animate(double) override {}

    SkColor getColor() { return fColor; }
    float getWidth() { return fWidth; }
    SkPoint getPoint() { return fPoint; }
    SkPoint getCoord() { return fCoordinate; }
    bool isSplit() { return fIsSplit; }
    void toggleIsSplit() { fIsSplit = !fIsSplit; }

private:
    SkPoint fPoint;
    SkPoint fCoordinate;
    SkColor fColor;
    float fWidth;
    bool fIsSplit;
};

class CanvasQuadraticSegment : public CanvasLinePoint {
public:
    CanvasQuadraticSegment(SkRandom* random, SkSize size, SkPoint* prev)
            : CanvasLinePoint(random, size, prev) {
        // Note: The construction of these points is odd but mirrors the Javascript code.

        // The chosen point from the base constructor is instead the control point.
        fPoint2 = this->getPoint();

        // Get another random point for the actual end point of the segment.
        this->setEndPoint(random, size, prev);
    }

    void append(SkPath* path) override {
        path->quadTo(fPoint2, this->getPoint());
    }

private:
    SkPoint fPoint2;
};

class CanvasBezierSegment : public CanvasLinePoint {
public:
    CanvasBezierSegment(SkRandom* random, SkSize size, SkPoint* prev)
            : CanvasLinePoint(random, size, prev) {
        // Note: The construction of these points is odd but mirrors the Javascript code.

        // The chosen point from the base constructor is instead the control point.
        fPoint2 = this->getPoint();

        // Get the second control point.
        this->setEndPoint(random, size, prev);
        fPoint3 = this->getPoint();

        // Get third random point for the actual end point of the segment.
        this->setEndPoint(random, size, prev);
    }

    void append(SkPath* path) override {
        path->cubicTo(fPoint2, fPoint3, this->getPoint());
    }

private:
    SkPoint fPoint2;
    SkPoint fPoint3;
};


std::unique_ptr<CanvasLinePoint> make_line_path(SkRandom* random, SkSize size, SkPoint* prev) {
    int choice = random->nextRangeU(0, 3);
    switch (choice) {
        case 0:
            return std::make_unique<CanvasQuadraticSegment>(random, size, prev);
            break;
        case 1:
            return std::make_unique<CanvasBezierSegment>(random, size, prev);
            break;
        case 2:
        case 3:
        default:
            return std::make_unique<CanvasLinePoint>(random, size, prev);
            break;
    }
}

class CanvasLinePathStage : public Stage {
public:
    CanvasLinePathStage(SkSize size)
            : Stage(size, /*startingObjectCount=*/5000, /*objectIncrement=*/1000) {
        this->initializeObjects();
    }

    ~CanvasLinePathStage() override = default;

    void draw(SkCanvas* canvas) override {
        canvas->clear(SK_ColorWHITE);

        SkPath currentPath;
        SkPaint paint;
        paint.setAntiAlias(true);
        paint.setStyle(SkPaint::kStroke_Style);
        for (size_t i = 0; i < fObjects.size(); ++i) {
            CanvasLinePoint* object = reinterpret_cast<CanvasLinePoint*>(fObjects[i].get());
            if (i == 0) {
                paint.setStrokeWidth(object->getWidth());
                paint.setColor(object->getColor());
                currentPath.moveTo(object->getPoint());
            } else {
                object->append(&currentPath);

                if (object->isSplit()) {
                    canvas->drawPath(currentPath, paint);

                    paint.setStrokeWidth(object->getWidth());
                    paint.setColor(object->getColor());
                    currentPath.reset();
                    currentPath.moveTo(object->getPoint());
                }

                if (fRandom.nextF() > 0.995) {
                    object->toggleIsSplit();
                }
            }
        }
        canvas->drawPath(currentPath, paint);
    }

    bool animate(double /*nanos*/) override {
        // Nothing to do, but return true so we redraw.
        return true;
    }

    std::unique_ptr<MMObject> createObject() override {
        if (fObjects.empty()) {
            return make_line_path(&fRandom, fSize, nullptr);
        } else {
            CanvasLinePoint* prevObject = reinterpret_cast<CanvasLinePoint*>(fObjects.back().get());
            SkPoint coord = prevObject->getCoord();
            return make_line_path(&fRandom, fSize, &coord);
        }
    }
};

///////////////////////////////////////////////////////////////////////////////////////////////////
// Bouncing Particles
///////////////////////////////////////////////////////////////////////////////////////////////////

SkPoint random_position(SkRandom* random, SkSize maxPosition) {
    return {(float)random->nextRangeU(0, maxPosition.width()),
            (float)random->nextRangeU(0, maxPosition.height())};
}

float random_angle(SkRandom* random) {
    return random->nextRangeF(0, 2*SK_FloatPI);
}

float random_velocity(SkRandom* random, float maxVelocity) {
    return random->nextRangeF(maxVelocity/8, maxVelocity);
}

class Rotater {
public:
    Rotater(float rotateInterval)
            : fTimeDelta(0)
            , fRotateInterval(rotateInterval) {}

    void next(float timeDelta) {
        fTimeDelta = SkScalarMod(fTimeDelta + timeDelta, fRotateInterval);
    }

    float degrees() {
        return (360 * fTimeDelta) / fRotateInterval;
    }

private:
    float fTimeDelta;
    float fRotateInterval;
};

Rotater random_rotater(SkRandom* random) {
    return Rotater(random->nextRangeF(10, 100));
}

SkPoint point_on_circle(float angle, float radius) {
    return {radius * SkScalarCos(angle), radius * SkScalarSin(angle)};
}

class BouncingParticle : public MMObject {
public:
    BouncingParticle(SkRandom* random, SkSize stageSize, SkSize particleSize, float maxVelocity)
            : fStageSize(stageSize)
            , fSize(particleSize)
            , fPosition(random_position(random,
                                        {stageSize.fWidth - particleSize.fWidth,
                                         stageSize.fHeight - particleSize.fHeight}))
            , fAngle(random_angle(random))
            , fVelocity(random_velocity(random, maxVelocity))
            , fRotater(random_rotater(random)) {
    }

    void animate(double deltaNanos) override {
        // TODO: consolidate and pass in millis to the Stages
        constexpr double kMillisPerNano = 0.0000001;
        fPosition += point_on_circle(fAngle, fVelocity * (deltaNanos * kMillisPerNano));
        fRotater.next(deltaNanos * kMillisPerNano);

        // If particle is going to move off right side
        if (fPosition.fX + fSize.width() > fStageSize.width()) {
            // If direction is East-South, go West-South.
            if (fAngle >= 0 && fAngle < SK_FloatPI / 2)
                fAngle = SK_FloatPI - fAngle;
            // If angle is East-North, go West-North.
            else if (fAngle > SK_FloatPI / 2 * 3)
                fAngle = fAngle - (fAngle - SK_FloatPI / 2 * 3) * 2;
            // Make sure the particle does not go outside the stage boundaries.
            fPosition.fX = fStageSize.width() - fSize.width();
        }

        // If particle is going to move off left side
        if (fPosition.fX < 0) {
            // If angle is West-South, go East-South.
            if (fAngle > SK_FloatPI / 2 && fAngle < SK_FloatPI)
                fAngle = SK_FloatPI - fAngle;
            // If angle is West-North, go East-North.
            else if (fAngle > SK_FloatPI && fAngle < SK_FloatPI / 2 * 3)
                fAngle = fAngle + (SK_FloatPI / 2 * 3 - fAngle) * 2;
            // Make sure the particle does not go outside the stage boundaries.
            fPosition.fX = 0;
        }

        // If particle is going to move off bottom side
        if (fPosition.fY + fSize.height() > fStageSize.height()) {
            // If direction is South, go North.
            if (fAngle > 0 && fAngle < SK_FloatPI)
                fAngle = SK_FloatPI * 2 - fAngle;
            // Make sure the particle does not go outside the stage boundaries.
            fPosition.fY = fStageSize.height() - fSize.height();
        }

        // If particle is going to move off top side
        if (fPosition.fY < 0) {
            // If direction is North, go South.
            if (fAngle > SK_FloatPI && fAngle < SK_FloatPI * 2)
                fAngle = fAngle - (fAngle - SK_FloatPI) * 2;
            // Make sure the particle does not go outside the stage boundaries.
            fPosition.fY = 0;
        }
    }

protected:
    SkSize fStageSize;
    SkSize fSize;
    SkPoint fPosition;
    float fAngle;
    float fVelocity;
    Rotater fRotater;
};

class BouncingParticlesStage : public Stage {
public:
    BouncingParticlesStage(SkSize size)
            : Stage(size, /*startingObjectCount=*/3000, /*objectIncrement=*/500)
            , fParticleSize({150, 150})
            , fMaxVelocity(10){
    }

    bool animate(double nanos) override {
        // The particles take delta time
        if (fLastTime < 0) {
            fLastTime = nanos;
        }
        for (size_t i = 0; i < fObjects.size(); ++i) {
            fObjects[i]->animate(nanos - fLastTime);
        }
        fLastTime = nanos;
        return true;
    }

protected:
    SkSize fParticleSize;
    float fMaxVelocity;
    double fLastTime = -1;
};


class BouncingTaggedImage : public BouncingParticle {
public:
    BouncingTaggedImage(SkRandom* random, SkSize stageSize, SkSize particleSize, float maxVelocity)
            : BouncingParticle(random, stageSize, particleSize, maxVelocity) {
        this->move();
        fRect = SkRect::MakeSize(fSize);
        fRect.offset(-fSize.width()/2, -fSize.height()/2);
    }

    void move() {
        fTransform = SkMatrix::RotateDeg(std::floor(fRotater.degrees()));
        fTransform.setTranslateX(fPosition.fX);
        fTransform.setTranslateY(fPosition.fY);
    }

    void animate(double deltaNanos) override {
        BouncingParticle::animate(deltaNanos);
        this->move();
    }

    void draw(SkCanvas* canvas) override {
        // handled by the Stage
    }

    const SkMatrix& transform() { return fTransform; }
    SkRect rect() { return fRect; }

private:
    SkMatrix fTransform;
    SkRect fRect;
};


class BouncingTaggedImagesStage : public BouncingParticlesStage {
public:
    BouncingTaggedImagesStage(SkSize size) : BouncingParticlesStage(size) {

        this->initializeObjects();
    }

    ~BouncingTaggedImagesStage() override = default;

    void initImages(SkCanvas* canvas) {
        const char* kImageSrcs[kImageCount] = {
            "images/ducky.jpg",
            "images/dog.jpg",
            "images/color_wheel.jpg",
            "images/mandrill_512_q075.jpg",
            "images/gainmap_iso21496_1.jpg",
        };

        auto rContext = canvas->recordingContext();
#if defined(SK_GRAPHITE)
        skgpu::graphite::Recorder* recorder = nullptr;
        recorder = canvas->recorder();
#endif
        for (int i = 0; i < kImageCount; ++i) {
            auto lazyYUV = sk_gpu_test::LazyYUVImage::Make(GetResourceAsData(kImageSrcs[i]),
                                                           skgpu::Mipmapped::kYes);
            SkASSERT(lazyYUV);
#if defined(SK_GRAPHITE)
            if (recorder) {
                fImages[i] = lazyYUV->refImage(recorder,
                                               sk_gpu_test::LazyYUVImage::Type::kFromPixmaps);
            } else
#endif
            {
                fImages[i] = lazyYUV->refImage(rContext,
                                               sk_gpu_test::LazyYUVImage::Type::kFromPixmaps);
            }
        }
    }

    void draw(SkCanvas* canvas) override {
        if (fNeedToInitImages) {
            this->initImages(canvas);
            fNeedToInitImages = false;
        }

        canvas->clear(SK_ColorWHITE);

        SkPaint paint;
        SkSamplingOptions sampling(SkFilterMode::kLinear,
                                   SkMipmapMode::kNearest);
        for (size_t i = 0; i < fObjects.size(); ++i) {
            BouncingTaggedImage* object = reinterpret_cast<BouncingTaggedImage*>(fObjects[i].get());

            canvas->save();
            canvas->concat(object->transform());
            canvas->drawImageRect(fImages[i % kImageCount], object->rect(), sampling, nullptr);

            canvas->restore();
        }
    }

    std::unique_ptr<MMObject> createObject() override {
        return std::make_unique<BouncingTaggedImage>(&fRandom, fSize, fParticleSize, fMaxVelocity);
    }

    void reset() {
        fNeedToInitImages = true;
    }

private:
    static constexpr int kImageCount = 5;

    bool fNeedToInitImages = true;
    sk_sp<SkImage> fImages[kImageCount];
};

///////////////////////////////////////////////////////////////////////////////////////////////////

class CanvasLinesSlide : public MotionMarkSlide {
public:
    CanvasLinesSlide() {fName = "MotionMarkCanvasLines"; }

    void load(SkScalar w, SkScalar h) override {
        fStage = std::make_unique<CanvasLineSegmentStage>(SkSize::Make(w, h));
    }
};

class CanvasArcsSlide : public MotionMarkSlide {
public:
    CanvasArcsSlide() {fName = "MotionMarkCanvasArcs"; }

    void load(SkScalar w, SkScalar h) override {
        fStage = std::make_unique<CanvasArcStage>(SkSize::Make(w, h));
    }
};

class PathsSlide : public MotionMarkSlide {
public:
    PathsSlide() {fName = "MotionMarkPaths"; }

    void load(SkScalar w, SkScalar h) override {
        fStage = std::make_unique<CanvasLinePathStage>(SkSize::Make(w, h));
    }
};

class BouncingTaggedImagesSlide : public MotionMarkSlide {
public:
    BouncingTaggedImagesSlide() {fName = "MotionMarkBouncingTaggedImages"; }

    void load(SkScalar w, SkScalar h) override {
        fStage = std::make_unique<BouncingTaggedImagesStage>(SkSize::Make(w, h));
    }

    void gpuTeardown() override {
        reinterpret_cast<BouncingTaggedImagesStage*>(fStage.get())->reset();
    }
};

DEF_SLIDE( return new CanvasLinesSlide(); )
DEF_SLIDE( return new CanvasArcsSlide(); )
DEF_SLIDE( return new PathsSlide(); )
DEF_SLIDE( return new BouncingTaggedImagesSlide(); )
