/*
  ==============================================================================

   This file is part of the JUCE examples.
   Copyright (c) 2022 - Raw Material Software Limited

   The code included in this file is provided under the terms of the ISC license
   http://www.isc.org/downloads/software-support-policy/isc-license. Permission
   To use, copy, modify, and/or distribute this software for any purpose with or
   without fee is hereby granted provided that the above copyright notice and
   this permission notice appear in all copies.

   THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
   WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
   PURPOSE, ARE DISCLAIMED.

  ==============================================================================
*/

/*******************************************************************************
 The block below describes the properties of this PIP. A PIP is a short snippet
 of code that can be read by the Projucer and used to generate a JUCE project.

 BEGIN_JUCE_PIP_METADATA

 name:             GraphicsDemo
 version:          1.0.0
 vendor:           JUCE
 website:          http://juce.com
 description:      Showcases various graphics features.

 dependencies:     juce_core, juce_data_structures, juce_events, juce_graphics,
                   juce_gui_basics
 exporters:        xcode_mac, vs2022, linux_make, androidstudio, xcode_iphone

 moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1

 type:             Component
 mainClass:        GraphicsDemo

 useLocalCopy:     1

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once

#include "../Assets/DemoUtilities.h"

//==============================================================================
/** Holds the various toggle buttons for the animation modes. */
class ControllersComponent final : public Component
{
public:
    ControllersComponent()
    {
        setOpaque (true);

        initialiseToggle (animatePosition, "Animate Position",  true);
        initialiseToggle (animateRotation, "Animate Rotation",  true);
        initialiseToggle (animateSize,     "Animate Size",      false);
        initialiseToggle (animateShear,    "Animate Shearing",  false);
        initialiseToggle (animateAlpha,    "Animate Alpha",     false);
        initialiseToggle (clipToRectangle, "Clip to Rectangle", false);
        initialiseToggle (clipToPath,      "Clip to Path",      false);
        initialiseToggle (clipToImage,     "Clip to Image",     false);
        initialiseToggle (quality,         "Higher quality image interpolation", false);
    }

    void paint (Graphics& g) override
    {
        g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground));
    }

    void resized() override
    {
        auto r = getLocalBounds().reduced (4);

        int buttonHeight = 22;

        auto columns = r.removeFromTop (buttonHeight * 4);
        auto col = columns.removeFromLeft (200);

        animatePosition.setBounds (col.removeFromTop (buttonHeight));
        animateRotation.setBounds (col.removeFromTop (buttonHeight));
        animateSize    .setBounds (col.removeFromTop (buttonHeight));
        animateShear   .setBounds (col.removeFromTop (buttonHeight));

        columns.removeFromLeft (20);
        col = columns.removeFromLeft (200);

        animateAlpha   .setBounds (col.removeFromTop (buttonHeight));
        clipToRectangle.setBounds (col.removeFromTop (buttonHeight));
        clipToPath     .setBounds (col.removeFromTop (buttonHeight));
        clipToImage    .setBounds (col.removeFromTop (buttonHeight));

        r.removeFromBottom (6);
        quality.setBounds (r.removeFromTop (buttonHeight));
    }

    void initialiseToggle (ToggleButton& b, const char* name, bool on)
    {
        addAndMakeVisible (b);
        b.setButtonText (name);
        b.setToggleState (on, dontSendNotification);
    }

    ToggleButton animateRotation, animatePosition, animateAlpha, animateSize, animateShear;
    ToggleButton clipToRectangle, clipToPath, clipToImage, quality;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControllersComponent)
};

//==============================================================================
class GraphicsDemoBase : public Component
{
public:
    GraphicsDemoBase (ControllersComponent& cc, const String& name)
        : Component (name),
          controls (cc)
    {
        displayFont = Font (Font::getDefaultMonospacedFontName(), 12.0f, Font::bold);
    }

    AffineTransform getTransform()
    {
        auto hw = 0.5f * (float) getWidth();
        auto hh = 0.5f * (float) getHeight();

        AffineTransform t;

        if (controls.animateRotation.getToggleState())
            t = t.rotated (rotation.getValue() * MathConstants<float>::twoPi);

        if (controls.animateSize.getToggleState())
            t = t.scaled (0.3f + size.getValue() * 2.0f);

        if (controls.animatePosition.getToggleState())
            t = t.translated (hw + hw * (offsetX.getValue() - 0.5f),
                              hh + hh * (offsetY.getValue() - 0.5f));
        else
            t = t.translated (hw, hh);

        if (controls.animateShear.getToggleState())
            t = t.sheared (shear.getValue() * 2.0f - 1.0f, 0.0f);

        return t;
    }

    float getAlpha() const
    {
        if (controls.animateAlpha.getToggleState())
            return alpha.getValue();

        return 1.0f;
    }

    void paint (Graphics& g) override
    {
        auto startTime = 0.0;

        {
            // A ScopedSaveState will return the Graphics context to the state it was at the time of
            // construction when it goes out of scope. We use it here to avoid clipping the fps text
            const Graphics::ScopedSaveState state (g);

            if (controls.clipToRectangle.getToggleState())  clipToRectangle (g);
            if (controls.clipToPath     .getToggleState())  clipToPath (g);
            if (controls.clipToImage    .getToggleState())  clipToImage (g);

            g.setImageResamplingQuality (controls.quality.getToggleState() ? Graphics::highResamplingQuality
                                                                           : Graphics::mediumResamplingQuality);

            // take a note of the time before the render
            startTime = Time::getMillisecondCounterHiRes();

            // then let the demo draw itself..
            drawDemo (g);
        }

        auto now = Time::getMillisecondCounterHiRes();
        auto filtering = 0.08;

        auto elapsedMs = now - startTime;
        averageTimeMs += (elapsedMs - averageTimeMs) * filtering;

        auto sinceLastRender = now - lastRenderStartTime;
        lastRenderStartTime = now;

        auto effectiveFPS = 1000.0 / averageTimeMs;
        auto actualFPS = sinceLastRender > 0 ? (1000.0 / sinceLastRender) : 0;
        averageActualFPS += (actualFPS - averageActualFPS) * filtering;

        GlyphArrangement ga;
        ga.addFittedText (displayFont,
                          "Time: " + String (averageTimeMs, 2)
                            + " ms\nEffective FPS: " + String (effectiveFPS, 1)
                            + "\nActual FPS: " + String (averageActualFPS, 1),
                          0, 10.0f, (float) getWidth() - 10.0f, (float) getHeight(), Justification::topRight, 3);

        g.setColour (Colours::white.withAlpha (0.5f));
        g.fillRect (ga.getBoundingBox (0, ga.getNumGlyphs(), true).getSmallestIntegerContainer().expanded (4));

        g.setColour (Colours::black);
        ga.draw (g);
    }

    virtual void drawDemo (Graphics&) = 0;

    void clipToRectangle (Graphics& g)
    {
        auto w = getWidth()  / 2;
        auto h = getHeight() / 2;

        auto x = (int) ((float) w * clipRectX.getValue());
        auto y = (int) ((float) h * clipRectY.getValue());

        g.reduceClipRegion (x, y, w, h);
    }

    void clipToPath (Graphics& g)
    {
        auto pathSize = (float) jmin (getWidth(), getHeight());

        Path p;
        p.addStar (Point<float> (clipPathX.getValue(),
                                 clipPathY.getValue()) * pathSize,
                   7,
                   pathSize * (0.5f + clipPathDepth.getValue()),
                   pathSize * 0.5f,
                   clipPathAngle.getValue());

        g.reduceClipRegion (p, AffineTransform());
    }

    void clipToImage (Graphics& g)
    {
        if (! clipImage.isValid())
            createClipImage();

        AffineTransform transform (AffineTransform::translation ((float) clipImage.getWidth()  / -2.0f,
                                                                 (float) clipImage.getHeight() / -2.0f)
                                   .rotated (clipImageAngle.getValue() * MathConstants<float>::twoPi)
                                   .scaled (2.0f + clipImageSize.getValue() * 3.0f)
                                   .translated ((float) getWidth()  * 0.5f,
                                                (float) getHeight() * 0.5f));

        g.reduceClipRegion (clipImage, transform);
    }

    void createClipImage()
    {
        clipImage = Image (Image::ARGB, 300, 300, true);

        Graphics g (clipImage);

        g.setGradientFill (ColourGradient (Colours::transparentBlack, 0, 0,
                                           Colours::black, 0, 300, false));

        for (int i = 0; i < 20; ++i)
            g.fillRect (Random::getSystemRandom().nextInt (200),
                        Random::getSystemRandom().nextInt (200),
                        Random::getSystemRandom().nextInt (100),
                        Random::getSystemRandom().nextInt (100));
    }

    //==============================================================================
    ControllersComponent& controls;

    SlowerBouncingNumber offsetX, offsetY, rotation, size, shear, alpha, clipRectX,
                         clipRectY, clipPathX, clipPathY, clipPathDepth, clipPathAngle,
                         clipImageX, clipImageY, clipImageAngle, clipImageSize;

    double lastRenderStartTime = 0.0, averageTimeMs = 0.0, averageActualFPS = 0.0;
    Image clipImage;
    Font displayFont;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GraphicsDemoBase)
};

//==============================================================================
class RectangleFillTypesDemo final : public GraphicsDemoBase
{
public:
    RectangleFillTypesDemo (ControllersComponent& cc)
        : GraphicsDemoBase (cc, "Fill Types: Rectangles")
    {}

    void drawDemo (Graphics& g) override
    {
        g.addTransform (getTransform());

        const int rectSize = jmin (getWidth(), getHeight()) / 2 - 20;

        g.setColour (colour1.withAlpha (getAlpha()));
        g.fillRect (-rectSize, -rectSize, rectSize, rectSize);

        g.setGradientFill (ColourGradient (colour1, 10.0f, (float) -rectSize,
                                           colour2, 10.0f + (float) rectSize, 0.0f, false));
        g.setOpacity (getAlpha());
        g.fillRect (10, -rectSize, rectSize, rectSize);

        g.setGradientFill (ColourGradient (colour1, (float) rectSize * -0.5f, 10.0f + (float) rectSize * 0.5f,
                                           colour2, 0, 10.0f + (float) rectSize, true));
        g.setOpacity (getAlpha());
        g.fillRect (-rectSize, 10, rectSize, rectSize);

        g.setGradientFill (ColourGradient (colour1, 10.0f, 10.0f,
                                           colour2, 10.0f + (float) rectSize, 10.0f + (float) rectSize, false));
        g.setOpacity (getAlpha());
        g.drawRect (10, 10, rectSize, rectSize, 5);
    }

    Colour colour1 { Colours::red }, colour2 { Colours::green };
};

//==============================================================================
class PathsDemo final : public GraphicsDemoBase
{
public:
    PathsDemo (ControllersComponent& cc, bool linear, bool radial)
        : GraphicsDemoBase (cc, String ("Paths") + (radial ? ": Radial Gradients"
                                                           : (linear ? ": Linear Gradients"
                                                                     : ": Solid"))),
          useLinearGradient (linear), useRadialGradient (radial)
    {
        logoPath = getJUCELogoPath();

        // rescale the logo path so that it's centred about the origin and has the right size.
        logoPath.applyTransform (RectanglePlacement (RectanglePlacement::centred)
                                 .getTransformToFit (logoPath.getBounds(),
                                                     Rectangle<float> (-120.0f, -120.0f, 240.0f, 240.0f)));

        // Surround it with some other shapes..
        logoPath.addStar ({ -300.0f, -50.0f }, 7, 30.0f, 70.0f, 0.1f);
        logoPath.addStar ({ 300.0f, 50.0f }, 6, 40.0f, 70.0f, 0.1f);
        logoPath.addEllipse (-100.0f, 150.0f, 200.0f, 140.0f);
        logoPath.addRectangle (-100.0f, -280.0f, 200.0f, 140.0f);
    }

    void drawDemo (Graphics& g) override
    {
        auto& p = logoPath;

        if (useLinearGradient || useRadialGradient)
        {
            Colour c1 (gradientColours[0].getValue(), gradientColours[1].getValue(), gradientColours[2].getValue(), 1.0f);
            Colour c2 (gradientColours[3].getValue(), gradientColours[4].getValue(), gradientColours[5].getValue(), 1.0f);
            Colour c3 (gradientColours[6].getValue(), gradientColours[7].getValue(), gradientColours[8].getValue(), 1.0f);

            auto x1 = gradientPositions[0].getValue() * (float) getWidth()  * 0.25f;
            auto y1 = gradientPositions[1].getValue() * (float) getHeight() * 0.25f;
            auto x2 = gradientPositions[2].getValue() * (float) getWidth()  * 0.75f;
            auto y2 = gradientPositions[3].getValue() * (float) getHeight() * 0.75f;

            ColourGradient gradient (c1, x1, y1,
                                     c2, x2, y2,
                                     useRadialGradient);

            gradient.addColour (gradientIntermediate.getValue(), c3);

            g.setGradientFill (gradient);
        }
        else
        {
            g.setColour (Colours::blue);
        }

        g.setOpacity (getAlpha());
        g.fillPath (p, getTransform());
    }

    Path logoPath;
    bool useLinearGradient, useRadialGradient;
    SlowerBouncingNumber gradientColours[9], gradientPositions[4], gradientIntermediate;
};

//==============================================================================
class StrokesDemo final : public GraphicsDemoBase
{
public:
    StrokesDemo (ControllersComponent& cc)
        : GraphicsDemoBase (cc, "Paths: Stroked")
    {}

    void drawDemo (Graphics& g) override
    {
        auto w = (float) getWidth();
        auto h = (float) getHeight();

        Path p;
        p.startNewSubPath (points[0].getValue() * w,
                           points[1].getValue() * h);

        for (int i = 2; i < numElementsInArray (points); i += 4)
            p.quadraticTo (points[i]    .getValue() * w,
                           points[i + 1].getValue() * h,
                           points[i + 2].getValue() * w,
                           points[i + 3].getValue() * h);

        p.closeSubPath();

        PathStrokeType stroke (0.5f + 10.0f * thickness.getValue());
        g.setColour (Colours::purple.withAlpha (getAlpha()));
        g.strokePath (p, stroke, AffineTransform());
    }

    SlowerBouncingNumber points[2 + 4 * 8], thickness;
};

//==============================================================================
class ImagesRenderingDemo final : public GraphicsDemoBase
{
public:
    ImagesRenderingDemo (ControllersComponent& cc, bool argb, bool tiled)
        : GraphicsDemoBase (cc, String ("Images") + (argb ? ": ARGB" : ": RGB") + (tiled ? " Tiled" : String() )),
          isArgb (argb), isTiled (tiled)
    {
        argbImage = getImageFromAssets ("juce_icon.png");
        rgbImage  = getImageFromAssets ("portmeirion.jpg");
    }

    void drawDemo (Graphics& g) override
    {
        auto image = isArgb ? argbImage : rgbImage;

        AffineTransform transform (AffineTransform::translation ((float) (image.getWidth()  / -2),
                                                                 (float) (image.getHeight() / -2))
                                   .followedBy (getTransform()));

        if (isTiled)
        {
            FillType fill (image, transform);
            fill.setOpacity (getAlpha());
            g.setFillType (fill);
            g.fillAll();
        }
        else
        {
            g.setOpacity (getAlpha());
            g.drawImageTransformed (image, transform, false);
        }
    }

    bool isArgb, isTiled;
    Image rgbImage, argbImage;
};

//==============================================================================
class GlyphsDemo final : public GraphicsDemoBase
{
public:
    GlyphsDemo (ControllersComponent& cc)
        : GraphicsDemoBase (cc, "Glyphs")
    {
        glyphs.addFittedText ({ 20.0f }, "The Quick Brown Fox Jumped Over The Lazy Dog",
                              -120, -50, 240, 100, Justification::centred, 2, 1.0f);
    }

    void drawDemo (Graphics& g) override
    {
        g.setColour (Colours::black.withAlpha (getAlpha()));
        glyphs.draw (g, getTransform());
    }

    GlyphArrangement glyphs;
};

//==============================================================================
class SVGDemo final : public GraphicsDemoBase
{
public:
    SVGDemo (ControllersComponent& cc)
        : GraphicsDemoBase (cc, "SVG")
    {
        createSVGDrawable();
    }

    void drawDemo (Graphics& g) override
    {
        if (Time::getCurrentTime().toMilliseconds() > lastSVGLoadTime.toMilliseconds() + 2000)
            createSVGDrawable();

        svgDrawable->draw (g, getAlpha(), getTransform());
    }

    void createSVGDrawable()
    {
        lastSVGLoadTime = Time::getCurrentTime();

        ZipFile icons (createAssetInputStream ("icons.zip").release(), true);

        // Load a random SVG file from our embedded icons.zip file.
        const std::unique_ptr<InputStream> svgFileStream (icons.createStreamForEntry (Random::getSystemRandom().nextInt (icons.getNumEntries())));

        if (svgFileStream.get() != nullptr)
        {
            svgDrawable = Drawable::createFromImageDataStream (*svgFileStream);

            if (svgDrawable != nullptr)
            {
                // to make our icon the right size, we'll set its bounding box to the size and position that we want.

                if (auto comp = dynamic_cast<DrawableComposite*> (svgDrawable.get()))
                    comp->setBoundingBox ({ -100.0f, -100.0f, 200.0f, 200.0f });
            }
        }
    }

    Time lastSVGLoadTime;
    std::unique_ptr<Drawable> svgDrawable;
};

//==============================================================================
class LinesDemo final : public GraphicsDemoBase
{
public:
    LinesDemo (ControllersComponent& cc)
        : GraphicsDemoBase (cc, "Lines")
    {}

    void drawDemo (Graphics& g) override
    {
        {
            RectangleList<float> verticalLines;
            verticalLines.ensureStorageAllocated (getWidth());

            auto pos = offset.getValue();

            for (int x = 0; x < getWidth(); ++x)
            {
                auto y = (float) getHeight() * 0.3f;
                auto length = y * std::abs (std::sin ((float) x / 100.0f + 2.0f * pos));
                verticalLines.addWithoutMerging (Rectangle<float> ((float) x, y - length * 0.5f, 1.0f, length));
            }

            g.setColour (Colours::blue.withAlpha (getAlpha()));
            g.fillRectList (verticalLines);
        }

        {
            RectangleList<float> horizontalLines;
            horizontalLines.ensureStorageAllocated (getHeight());

            auto pos = offset.getValue();

            for (int y = 0; y < getHeight(); ++y)
            {
                auto x = (float) getWidth() * 0.3f;
                auto length = x * std::abs (std::sin ((float) y / 100.0f + 2.0f * pos));
                horizontalLines.addWithoutMerging (Rectangle<float> (x - length * 0.5f, (float) y, length, 1.0f));
            }

            g.setColour (Colours::green.withAlpha (getAlpha()));
            g.fillRectList (horizontalLines);
        }

        g.setColour (Colours::red.withAlpha (getAlpha()));

        auto w = (float) getWidth();
        auto h = (float) getHeight();

        g.drawLine (positions[0].getValue() * w,
                    positions[1].getValue() * h,
                    positions[2].getValue() * w,
                    positions[3].getValue() * h);

        g.drawLine (positions[4].getValue() * w,
                    positions[5].getValue() * h,
                    positions[6].getValue() * w,
                    positions[7].getValue() * h);
    }

    SlowerBouncingNumber offset, positions[8];
};

//==============================================================================
class DemoHolderComponent final : public Component,
                                  private Timer
{
public:
    DemoHolderComponent()
    {
        setOpaque (true);
    }

    void paint (Graphics& g) override
    {
        g.fillCheckerBoard (getLocalBounds().toFloat(), 48.0f, 48.0f,
                            Colours::lightgrey, Colours::white);
    }

    void timerCallback() override
    {
        if (currentDemo != nullptr)
            currentDemo->repaint();
    }

    void setDemo (GraphicsDemoBase* newDemo)
    {
        if (currentDemo != nullptr)
            removeChildComponent (currentDemo);

        currentDemo = newDemo;

        if (currentDemo != nullptr)
        {
            addAndMakeVisible (currentDemo);
            startTimerHz (60);
            resized();
        }
    }

    void resized() override
    {
        if (currentDemo != nullptr)
            currentDemo->setBounds (getLocalBounds());
    }

private:
    GraphicsDemoBase* currentDemo = nullptr;
};

//==============================================================================
class TestListComponent final : public Component,
                                private ListBoxModel
{
public:
    TestListComponent (DemoHolderComponent& holder, ControllersComponent& controls)
        : demoHolder (holder)
    {
        demos.add (new PathsDemo (controls, false, true));
        demos.add (new PathsDemo (controls, true,  false));
        demos.add (new PathsDemo (controls, false, false));
        demos.add (new RectangleFillTypesDemo (controls));
        demos.add (new StrokesDemo (controls));
        demos.add (new ImagesRenderingDemo (controls, false, false));
        demos.add (new ImagesRenderingDemo (controls, false, true));
        demos.add (new ImagesRenderingDemo (controls, true,  false));
        demos.add (new ImagesRenderingDemo (controls, true,  true));
        demos.add (new GlyphsDemo (controls));
        demos.add (new SVGDemo    (controls));
        demos.add (new LinesDemo  (controls));

        addAndMakeVisible (listBox);
        listBox.setTitle ("Test List");
        listBox.setModel (this);
        listBox.selectRow (0);
    }

    void resized() override
    {
        listBox.setBounds (getLocalBounds());
    }

    int getNumRows() override
    {
        return demos.size();
    }

    void paintListBoxItem (int rowNumber, Graphics& g, int width, int height, bool rowIsSelected) override
    {
        if (demos[rowNumber] == nullptr)
            return;

        if (rowIsSelected)
            g.fillAll (Colour::contrasting (findColour (ListBox::textColourId),
                                            findColour (ListBox::backgroundColourId)));

        g.setColour (findColour (ListBox::textColourId));
        g.setFont (14.0f);
        g.drawFittedText (getNameForRow (rowNumber), 8, 0, width - 10, height, Justification::centredLeft, 2);
    }

    String getNameForRow (int rowNumber) override
    {
        if (auto* demo = demos[rowNumber])
            return demo->getName();

        return {};
    }

    void selectedRowsChanged (int lastRowSelected) override
    {
        demoHolder.setDemo (demos [lastRowSelected]);
    }

private:
    DemoHolderComponent& demoHolder;
    ListBox listBox;
    OwnedArray<GraphicsDemoBase> demos;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TestListComponent)
};

//==============================================================================
class GraphicsDemo final : public Component
{
public:
    GraphicsDemo()
        : testList (demoHolder, controllersComponent)
    {
        setOpaque (true);

        addAndMakeVisible (demoHolder);
        addAndMakeVisible (controllersComponent);
        addAndMakeVisible (performanceDisplay);
        addAndMakeVisible (testList);

        setSize (750, 750);
    }

    void paint (Graphics& g) override
    {
        g.fillAll (Colours::grey);
    }

    void resized() override
    {
        auto area = getLocalBounds();

        controllersComponent.setBounds (area.removeFromBottom (150));
        testList            .setBounds (area.removeFromRight (150));
        demoHolder          .setBounds (area);
        performanceDisplay  .setBounds (area.removeFromTop (20).removeFromRight (100));
    }

private:
    ControllersComponent controllersComponent;
    DemoHolderComponent demoHolder;
    Label performanceDisplay;
    TestListComponent testList;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GraphicsDemo)
};
