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

   This file is part of the JUCE examples.
   Copyright (c) 2020 - 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:             BlocksMonitorDemo
 version:          1.0.0
 vendor:           JUCE
 website:          http://juce.com
 description:      Application to monitor Blocks devices.

 dependencies:     juce_audio_basics, juce_audio_devices, juce_audio_formats,
                   juce_audio_processors, juce_audio_utils, juce_blocks_basics,
                   juce_core, juce_data_structures, juce_events, juce_graphics,
                   juce_gui_basics, juce_gui_extra
 exporters:        xcode_mac, vs2019, linux_make, xcode_iphone

 moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1

 type:             Component
 mainClass:        BlocksMonitorDemo

 useLocalCopy:     1

 END_JUCE_PIP_METADATA

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

#pragma once


//==============================================================================
/**
    Base class that renders a Block on the screen
*/
class BlockComponent : public Component,
                       public SettableTooltipClient,
                       private TouchSurface::Listener,
                       private ControlButton::Listener,
                       private Timer
{
public:
    BlockComponent (Block::Ptr blockToUse)
        : block (blockToUse)
    {
        updateStatsAndTooltip();

        // Register BlockComponent as a listener to the touch surface
        if (auto touchSurface = block->getTouchSurface())
            touchSurface->addListener (this);

        // Register BlockComponent as a listener to any buttons
        for (auto button : block->getButtons())
            button->addListener (this);

        // If this is a Lightpad then set the grid program to be blank
        if (block->getLEDGrid() != nullptr)
            block->setProgram (std::make_unique<BitmapLEDProgram>(*block));

        // If this is a Lightpad then redraw it at 25Hz
        if (block->getType() == Block::lightPadBlock)
            startTimerHz (25);

        // Make sure the component can't go offscreen if it is draggable
        constrainer.setMinimumOnscreenAmounts (50, 50, 50, 50);
    }

    ~BlockComponent() override
    {
        // Remove any listeners
        if (auto touchSurface = block->getTouchSurface())
            touchSurface->removeListener (this);

        for (auto button : block->getButtons())
            button->removeListener (this);
    }

    /** Called periodically to update the tooltip with information about the Block */
    void updateStatsAndTooltip()
    {
        // Get the battery level of this Block and inform any subclasses
        auto batteryLevel = block->getBatteryLevel();
        handleBatteryLevelUpdate (batteryLevel);

        // Update the tooltip
        setTooltip ("Name = "          + block->getDeviceDescription() + "\n"
                  + "UID = "           + String (block->uid) + "\n"
                  + "Serial number = " + block->serialNumber + "\n"
                  + "Battery level = " + String ((int) (batteryLevel * 100)) + "%"
                  + (block->isBatteryCharging() ? "++"
                                                : "--"));
    }

    /** Subclasses should override this to paint the Block object on the screen */
    void paint (Graphics&) override = 0;

    /** Subclasses can override this to receive button down events from the Block */
    virtual void handleButtonPressed  (ControlButton::ButtonFunction, uint32) {}

    /** Subclasses can override this to receive button up events from the Block */
    virtual void handleButtonReleased (ControlButton::ButtonFunction, uint32) {}

    /** Subclasses can override this to receive touch events from the Block */
    virtual void handleTouchChange (TouchSurface::Touch) {}

    /** Subclasses can override this to battery level updates from the Block */
    virtual void handleBatteryLevelUpdate (float) {}

    /** The Block object that this class represents */
    Block::Ptr block;

    //==============================================================================
    /** Returns an integer index corresponding to a physical position on the hardware
        for each type of Control Block. */
    static int controlButtonFunctionToIndex (ControlButton::ButtonFunction f)
    {
        using CB = ControlButton;

        static Array<ControlButton::ButtonFunction> map[] =
        {
            { CB::mode,     CB::button0,  CB::velocitySensitivity },
            { CB::volume,   CB::button1,  CB::glideSensitivity    },
            { CB::scale,    CB::button2,  CB::slideSensitivity,  CB::click       },
            { CB::chord,    CB::button3,  CB::pressSensitivity,  CB::snap        },
            { CB::arp,      CB::button4,  CB::liftSensitivity,   CB::back        },
            { CB::sustain,  CB::button5,  CB::fixedVelocity,     CB::playOrPause },
            { CB::octave,   CB::button6,  CB::glideLock,         CB::record      },
            { CB::love,     CB::button7,  CB::pianoMode,         CB::learn       },
            { CB::up   },
            { CB::down }
        };

        for (int i = 0; i < numElementsInArray (map); ++i)
            if (map[i].contains (f))
                return i;

        return -1;
    }

    Point<float> getOffsetForPort (Block::ConnectionPort port)
    {
        using e = Block::ConnectionPort::DeviceEdge;

        switch (rotation)
        {
            case 0:
            {
                switch (port.edge)
                {
                    case e::north:
                        return { static_cast<float> (port.index), 0.0f };
                    case e::east:
                        return { static_cast<float> (block->getWidth()), static_cast<float> (port.index) };
                    case e::south:
                        return { static_cast<float> (port.index), static_cast<float> (block->getHeight()) };
                    case e::west:
                        return { 0.0f, static_cast<float> (port.index) };
                    default:
                        break;
                }

                break;
            }
            case 90:
            {
                switch (port.edge)
                {
                    case e::north:
                        return { 0.0f, static_cast<float> (port.index) };
                    case e::east:
                        return { static_cast<float> (-1.0f - (float) port.index), static_cast<float> (block->getWidth()) };
                    case e::south:
                        return { static_cast<float> (0.0f - (float) block->getHeight()), static_cast<float> (port.index) };
                    case e::west:
                        return { static_cast<float> (-1.0f - (float) port.index), 0.0f };
                    default:
                        break;
                }

                break;
            }
            case 180:
            {
                switch (port.edge)
                {
                    case e::north:
                        return { static_cast<float> (-1.0f - (float) port.index), 0.0f };
                    case e::east:
                        return { static_cast<float> (0.0f - (float) block->getWidth()), static_cast<float> (-1.0f - (float) port.index) };
                    case e::south:
                        return { static_cast<float> (-1.0f - (float) port.index), static_cast<float> (0.0f - (float) block->getHeight()) };
                    case e::west:
                        return { 0.0f, static_cast<float> (-1.0f - (float) port.index) };
                    default:
                        break;
                }

                break;
            }
            case 270:
            {
                switch (port.edge)
                {
                    case e::north:
                        return { 0.0f, static_cast<float> (-1.0f - (float) port.index) };
                    case e::east:
                        return { static_cast<float> (port.index), static_cast<float> (0 - (float) block->getWidth()) };
                    case e::south:
                        return { static_cast<float> (block->getHeight()), static_cast<float> (-1.0f - (float) port.index) };
                    case e::west:
                        return { static_cast<float> (port.index), 0.0f };
                    default:
                        break;
                }

                break;
            }

            default:
                break;
        }

        return {};
    }

    int rotation = 0;
    Point<float> topLeft = { 0.0f, 0.0f };

private:
    /** Used to call repaint() periodically */
    void timerCallback() override   { repaint(); }

    /** Overridden from TouchSurface::Listener */
    void touchChanged (TouchSurface&, const TouchSurface::Touch& t) override { handleTouchChange (t); }

    /** Overridden from ControlButton::Listener */
    void buttonPressed  (ControlButton& b, Block::Timestamp t) override      { handleButtonPressed  (b.getType(), t); }

    /** Overridden from ControlButton::Listener */
    void buttonReleased (ControlButton& b, Block::Timestamp t) override      { handleButtonReleased (b.getType(), t); }

    /** Overridden from MouseListener. Prepares the master Block component for dragging. */
    void mouseDown (const MouseEvent& e) override
    {
        if (block->isMasterBlock())
            componentDragger.startDraggingComponent (this, e);
    }

    /** Overridden from MouseListener. Drags the master Block component */
    void mouseDrag (const MouseEvent& e) override
    {
        if (block->isMasterBlock())
        {
            componentDragger.dragComponent (this, e, &constrainer);
            getParentComponent()->resized();
        }
    }

    ComponentDragger componentDragger;
    ComponentBoundsConstrainer constrainer;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BlockComponent)
};

//==============================================================================
/**
    Class that renders a Lightpad on the screen
*/
class LightpadComponent   : public BlockComponent
{
public:
    LightpadComponent (Block::Ptr blockToUse)
        : BlockComponent (blockToUse)
    {}

    void paint (Graphics& g) override
    {
        auto r = getLocalBounds().toFloat();

        // clip the drawing area to only draw in the block area
        {
            Path clipArea;
            clipArea.addRoundedRectangle (r, r.getWidth() / 20.0f);

            g.reduceClipRegion (clipArea);
        }

        // Fill a black square for the Lightpad
        g.fillAll (Colours::black);

        // size ration between physical and on-screen blocks
        Point<float> ratio (r.getWidth()  / (float) block->getWidth(),
                            r.getHeight() / (float) block->getHeight());

        auto maxCircleSize = (float) block->getWidth() / 3.0f;

        // iterate over the list of current touches and draw them on the onscreen Block
        for (auto touch : touches)
        {
            auto circleSize = touch.touch.z * maxCircleSize;

            Point<float> touchPosition (touch.touch.x,
                                        touch.touch.y);

            auto blob = Rectangle<float> (circleSize, circleSize)
                           .withCentre (touchPosition) * ratio;

            ColourGradient cg (colourArray[touch.touch.index],  blob.getCentreX(), blob.getCentreY(),
                               Colours::transparentBlack,       blob.getRight(),   blob.getBottom(),
                               true);

            g.setGradientFill (cg);
            g.fillEllipse (blob);
        }
    }

    void handleTouchChange (TouchSurface::Touch touch) override { touches.updateTouch (touch); }

private:
    /** An Array of colours to use for touches */
    Array<Colour> colourArray = { Colours::red,
                                  Colours::blue,
                                  Colours::green,
                                  Colours::yellow,
                                  Colours::white,
                                  Colours::hotpink,
                                  Colours::mediumpurple };

    /** A list of current Touch events */
    TouchList<TouchSurface::Touch> touches;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LightpadComponent)
};


//==============================================================================
/**
    Class that renders a Control Block on the screen
*/
class ControlBlockComponent   : public BlockComponent
{
public:
    ControlBlockComponent (Block::Ptr blockToUse)
        : BlockComponent (blockToUse),
          numLeds (block->getLEDRow()->getNumLEDs())
    {
        addAndMakeVisible (roundedRectangleButton);

        // Display the battery level on the LEDRow
        auto numLedsToTurnOn = static_cast<int> ((float) numLeds * block->getBatteryLevel());

        // add LEDs
        for (auto i = 0; i < numLeds; ++i)
        {
            auto ledComponent = new LEDComponent();
            ledComponent->setOnState (i < numLedsToTurnOn);

            addAndMakeVisible (leds.add (ledComponent));
        }

        previousNumLedsOn = numLedsToTurnOn;

        // add buttons
        for (auto i = 0; i < 8; ++i)
            addAndMakeVisible (circleButtons[i]);
    }

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

        auto rowHeight   = r.getHeight() / 5;
        auto ledWidth    = (r.getWidth() - 70) / numLeds;
        auto buttonWidth = (r.getWidth() - 40) / 5;

        auto row = r;

        auto ledRow     = row.removeFromTop (rowHeight)    .withSizeKeepingCentre (r.getWidth(), ledWidth);
        auto buttonRow1 = row.removeFromTop (rowHeight * 2).withSizeKeepingCentre (r.getWidth(), buttonWidth);
        auto buttonRow2 = row.removeFromTop (rowHeight * 2).withSizeKeepingCentre (r.getWidth(), buttonWidth);

        for (auto* led : leds)
        {
            led->setBounds (ledRow.removeFromLeft (ledWidth).reduced (2));
            ledRow.removeFromLeft (5);
        }

        for (auto i = 0; i < 5; ++i)
        {
            circleButtons[i].setBounds (buttonRow1.removeFromLeft (buttonWidth).reduced (2));
            buttonRow1.removeFromLeft (10);
        }

        for (auto i = 5; i < 8; ++i)
        {
            circleButtons[i].setBounds (buttonRow2.removeFromLeft (buttonWidth).reduced (2));
            buttonRow2.removeFromLeft (10);
        }

        roundedRectangleButton.setBounds (buttonRow2);
    }

    void paint (Graphics& g) override
    {
        auto r = getLocalBounds().toFloat();

        // Fill a black rectangle for the Control Block
        g.setColour (Colours::black);
        g.fillRoundedRectangle (r, r.getWidth() / 20.0f);
    }

    void handleButtonPressed  (ControlButton::ButtonFunction function, uint32) override
    {
        displayButtonInteraction (controlButtonFunctionToIndex (function), true);
    }

    void handleButtonReleased (ControlButton::ButtonFunction function, uint32) override
    {
        displayButtonInteraction (controlButtonFunctionToIndex (function), false);
    }

    void handleBatteryLevelUpdate (float batteryLevel) override
    {
        // Update the number of LEDs that are on to represent the battery level
        auto numLedsOn = static_cast<int> ((float) numLeds * batteryLevel);

        if (numLedsOn != previousNumLedsOn)
            for (auto i = 0; i < numLeds; ++i)
                leds.getUnchecked (i)->setOnState (i < numLedsOn);

        previousNumLedsOn = numLedsOn;
        repaint();
    }

private:
    //==============================================================================
    /**
        Base class that renders a Control Block button
    */
    struct ControlBlockSubComponent   : public Component,
                                        public TooltipClient
    {
        ControlBlockSubComponent (Colour componentColourToUse)
            : componentColour (componentColourToUse)
        {}

        /** Subclasses should override this to paint the button on the screen */
        void paint (Graphics&) override = 0;

        /** Sets the colour of the button */
        void setColour (Colour c)   { componentColour = c; }

        /** Sets the on state of the button */
        void setOnState (bool isOn)
        {
            onState = isOn;
            repaint();
        }

        /** Returns the Control Block tooltip */
        String getTooltip() override
        {
            for (Component* comp = this; comp != nullptr; comp = comp->getParentComponent())
                if (auto* sttc = dynamic_cast<SettableTooltipClient*> (comp))
                    return sttc->getTooltip();

            return {};
        }

        //==============================================================================
        Colour componentColour;
        bool onState = false;

        //==============================================================================
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControlBlockSubComponent)
    };

    /**
        Class that renders a Control Block LED on the screen
    */
    struct LEDComponent  : public ControlBlockSubComponent
    {
        LEDComponent() : ControlBlockSubComponent (Colours::green) {}

        void paint (Graphics& g) override
        {
            g.setColour (componentColour.withAlpha (onState ? 1.0f : 0.2f));
            g.fillEllipse (getLocalBounds().toFloat());
        }
    };

    /**
        Class that renders a Control Block single circular button on the screen
    */
    struct CircleButtonComponent  : public ControlBlockSubComponent
    {
        CircleButtonComponent() : ControlBlockSubComponent (Colours::blue) {}

        void paint (Graphics& g) override
        {
            g.setColour (componentColour.withAlpha (onState ? 1.0f : 0.2f));
            g.fillEllipse (getLocalBounds().toFloat());
        }
    };

    /**
        Class that renders a Control Block rounded rectangular button containing two buttons
        on the screen
    */
    struct RoundedRectangleButtonComponent  : public ControlBlockSubComponent
    {
        RoundedRectangleButtonComponent() : ControlBlockSubComponent (Colours::blue) {}

        void paint (Graphics& g) override
        {
            auto r = getLocalBounds().toFloat();

            g.setColour (componentColour.withAlpha (0.2f));
            g.fillRoundedRectangle (r.toFloat(), 20.0f);
            g.setColour (componentColour.withAlpha (1.0f));

            // is a button pressed?
            if (doubleButtonOnState[0] || doubleButtonOnState[1])
            {
                auto semiButtonWidth = r.getWidth() / 2.0f;

                auto semiButtonBounds = r.withWidth (semiButtonWidth)
                                         .withX (doubleButtonOnState[1] ? semiButtonWidth : 0)
                                         .reduced (5.0f, 2.0f);

                g.fillEllipse (semiButtonBounds);
            }
        }

        void setPressedState (bool isPressed, int button)
        {
            doubleButtonOnState[button] = isPressed;
            repaint();
        }

    private:
        bool doubleButtonOnState[2] = { false, false };
    };

    /** Displays a button press or release interaction for a button at a given index */
    void displayButtonInteraction (int buttonIndex, bool isPressed)
    {
        if (! isPositiveAndBelow (buttonIndex, 10))
            return;

        if (buttonIndex >= 8)
            roundedRectangleButton.setPressedState (isPressed, buttonIndex == 8);
        else
            circleButtons[buttonIndex].setOnState (isPressed);
    }

    //==============================================================================
    int numLeds;
    OwnedArray<LEDComponent> leds;
    CircleButtonComponent circleButtons[8];
    RoundedRectangleButtonComponent roundedRectangleButton;
    int previousNumLedsOn;

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControlBlockComponent)
};

//==============================================================================
/**
    The main component where the Block components will be displayed
*/
class BlocksMonitorDemo   : public Component,
                            public TopologySource::Listener,
                            private Timer
{
public:
    BlocksMonitorDemo()
    {
        noBlocksLabel.setText ("No BLOCKS connected...", dontSendNotification);
        noBlocksLabel.setJustificationType (Justification::centred);

        zoomOutButton.setButtonText ("+");
        zoomOutButton.onClick = [this] { blockUnitInPixels = (int) ((float) blockUnitInPixels * 1.05f); resized(); };
        zoomOutButton.setAlwaysOnTop (true);

        zoomInButton.setButtonText ("-");
        zoomInButton.onClick = [this] { blockUnitInPixels = (int) ((float) blockUnitInPixels * 0.95f); resized(); };
        zoomInButton.setAlwaysOnTop (true);

        // Register BlocksMonitorDemo as a listener to the PhysicalTopologySource object
        topologySource.addListener (this);

        startTimer (10000);

        addAndMakeVisible (noBlocksLabel);
        addAndMakeVisible (zoomOutButton);
        addAndMakeVisible (zoomInButton);

       #if JUCE_IOS
        connectButton.setButtonText ("Connect");
        connectButton.onClick = [] { BluetoothMidiDevicePairingDialogue::open(); };
        connectButton.setAlwaysOnTop (true);
        addAndMakeVisible (connectButton);
       #endif

        setSize (600, 600);

        topologyChanged();
    }

    ~BlocksMonitorDemo() override
    {
        topologySource.removeListener (this);
    }

    void paint (Graphics&) override {}

    void resized() override
    {
       #if JUCE_IOS
        connectButton.setBounds (getRight() - 100, 20, 80, 30);
       #endif

        noBlocksLabel.setVisible (false);
        auto numBlockComponents = blockComponents.size();

        // If there are no currently connected Blocks then display some text on the screen
        if (masterBlockComponent == nullptr || numBlockComponents == 0)
        {
            noBlocksLabel.setVisible (true);
            noBlocksLabel.setBounds (0, (getHeight() / 2) - 50, getWidth(), 100);
            return;
        }

        zoomOutButton.setBounds (10, getHeight() - 40, 40, 30);
        zoomInButton.setBounds  (zoomOutButton.getRight(), zoomOutButton.getY(), 40, 30);

        if (isInitialResized)
        {
            // Work out the area needed in terms of Block units
            Rectangle<float> maxArea;
            for (auto blockComponent : blockComponents)
            {
                auto topLeft = blockComponent->topLeft;
                auto rotation = blockComponent->rotation;
                auto blockSize = 0;

                if (rotation == 180)
                    blockSize = blockComponent->block->getWidth();
                else if (rotation == 90)
                    blockSize = blockComponent->block->getHeight();

                if (topLeft.x - (float) blockSize < maxArea.getX())
                    maxArea.setX (topLeft.x - (float) blockSize);

                blockSize = 0;
                if (rotation == 0)
                    blockSize = blockComponent->block->getWidth();
                else if (rotation == 270)
                    blockSize = blockComponent->block->getHeight();

                if (topLeft.x + (float) blockSize > maxArea.getRight())
                    maxArea.setWidth (topLeft.x + (float) blockSize);

                blockSize = 0;
                if (rotation == 180)
                    blockSize = blockComponent->block->getHeight();
                else if (rotation == 270)
                    blockSize = blockComponent->block->getWidth();

                if (topLeft.y - (float) blockSize < maxArea.getY())
                    maxArea.setY (topLeft.y - (float) blockSize);

                blockSize = 0;
                if (rotation == 0)
                    blockSize = blockComponent->block->getHeight();
                else if (rotation == 90)
                    blockSize = blockComponent->block->getWidth();

                if (topLeft.y + (float) blockSize > maxArea.getBottom())
                    maxArea.setHeight (topLeft.y + (float) blockSize);
            }

            auto totalWidth  = std::abs (maxArea.getX()) + maxArea.getWidth();
            auto totalHeight = std::abs (maxArea.getY()) + maxArea.getHeight();

            blockUnitInPixels = static_cast<int> (jmin (((float) getHeight() / totalHeight) - 50, ((float) getWidth() / totalWidth) - 50));

            masterBlockComponent->centreWithSize (masterBlockComponent->block->getWidth()  * blockUnitInPixels,
                                                  masterBlockComponent->block->getHeight() * blockUnitInPixels);

            isInitialResized = false;
        }
        else
        {
            masterBlockComponent->setSize (masterBlockComponent->block->getWidth() * blockUnitInPixels, masterBlockComponent->block->getHeight() * blockUnitInPixels);
        }

        for (auto blockComponent : blockComponents)
        {
            if (blockComponent == masterBlockComponent)
                continue;

            blockComponent->setBounds (masterBlockComponent->getX() + static_cast<int> (blockComponent->topLeft.x * (float) blockUnitInPixels),
                                       masterBlockComponent->getY() + static_cast<int> (blockComponent->topLeft.y * (float) blockUnitInPixels),
                                       blockComponent->block->getWidth()  * blockUnitInPixels,
                                       blockComponent->block->getHeight() * blockUnitInPixels);

            if (blockComponent->rotation != 0)
                blockComponent->setTransform (AffineTransform::rotation (static_cast<float> (degreesToRadians (blockComponent->rotation)),
                                                                         static_cast<float> (blockComponent->getX()),
                                                                         static_cast<float> (blockComponent->getY())));
        }
    }

    /** Overridden from TopologySource::Listener, called when the topology changes */
    void topologyChanged() override
    {
        // Clear the array of Block components
        blockComponents.clear();
        masterBlockComponent = nullptr;

        // Get the current topology
        auto topology = topologySource.getCurrentTopology();

        // Create a BlockComponent object for each Block object and store a pointer to the master
        for (auto& block : topology.blocks)
        {
            if (auto* blockComponent = createBlockComponent (block))
            {
                addAndMakeVisible (blockComponents.add (blockComponent));

                if (blockComponent->block->isMasterBlock())
                    masterBlockComponent = blockComponent;
            }
        }

        // Must have a master Block!
        if (topology.blocks.size() > 0)
            jassert (masterBlockComponent != nullptr);

        // Calculate the relative position and rotation for each Block
        positionBlocks (topology);

        // Update the display
        isInitialResized = true;
        resized();
    }

private:
    /** Creates a BlockComponent object for a new Block and adds it to the content component */
    BlockComponent* createBlockComponent (Block::Ptr newBlock)
    {
        auto type = newBlock->getType();

        if (type == Block::lightPadBlock)
            return new LightpadComponent (newBlock);

        if (type == Block::loopBlock || type == Block::liveBlock
            || type == Block::touchBlock || type == Block::developerControlBlock)
            return new ControlBlockComponent (newBlock);

        // Should only be connecting a Lightpad or Control Block!
        jassertfalse;
        return nullptr;
    }

    /** Periodically updates the displayed BlockComponent tooltips */
    void timerCallback() override
    {
        for (auto c : blockComponents)
            c->updateStatsAndTooltip();
    }

    /** Calculates the position and rotation of each connected Block relative to the master Block */
    void positionBlocks (BlockTopology topology)
    {
        if (masterBlockComponent == nullptr)
            return;

        Array<BlockComponent*> blocksConnectedToMaster;

        auto maxDelta = std::numeric_limits<float>::max();
        auto maxLoops = 50;

        // Store all the connections to the master Block
        Array<BlockDeviceConnection> masterBlockConnections;
        for (auto connection : topology.connections)
            if (connection.device1 == masterBlockComponent->block->uid || connection.device2 == masterBlockComponent->block->uid)
                masterBlockConnections.add (connection);

        // Position all the Blocks that are connected to the master Block
        while (maxDelta > 0.001f && --maxLoops)
        {
            maxDelta = 0.0f;

            // Loop through each connection on the master Block
            for (auto connection : masterBlockConnections)
            {
                // Work out whether the master Block is device 1 or device 2 in the BlockDeviceConnection struct
                bool isDevice1 = true;
                if (masterBlockComponent->block->uid == connection.device2)
                    isDevice1 = false;

                // Get the connected ports
                auto masterPort = isDevice1 ? connection.connectionPortOnDevice1 : connection.connectionPortOnDevice2;
                auto otherPort  = isDevice1 ? connection.connectionPortOnDevice2 : connection.connectionPortOnDevice1;

                for (auto otherBlockComponent : blockComponents)
                {
                    // Get the other block
                    if (otherBlockComponent->block->uid == (isDevice1 ? connection.device2 : connection.device1))
                    {
                        blocksConnectedToMaster.addIfNotAlreadyThere (otherBlockComponent);

                        // Get the rotation of the other Block relative to the master Block
                        otherBlockComponent->rotation = getRotation (masterPort.edge, otherPort.edge);

                        // Get the offsets for the connected ports
                        auto masterBlockOffset = masterBlockComponent->getOffsetForPort (masterPort);
                        auto otherBlockOffset  = otherBlockComponent->topLeft + otherBlockComponent->getOffsetForPort (otherPort);

                        // Work out the distance between the two connected ports
                        auto delta = masterBlockOffset - otherBlockOffset;

                        // Move the other block half the distance to the connection
                        otherBlockComponent->topLeft += delta / 2.0f;

                        // Work out whether we are close enough for the loop to end
                        maxDelta = jmax (maxDelta, std::abs (delta.x), std::abs (delta.y));
                    }
                }
            }
        }

        // Check if there are any Blocks that have not been positioned yet
        Array<BlockComponent*> unpositionedBlocks;

        for (auto blockComponent : blockComponents)
            if (blockComponent != masterBlockComponent && ! blocksConnectedToMaster.contains (blockComponent))
                unpositionedBlocks.add (blockComponent);

        if (unpositionedBlocks.size() > 0)
        {
            // Reset the loop conditions
            maxDelta = std::numeric_limits<float>::max();
            maxLoops = 50;

            // Position all the remaining Blocks
            while (maxDelta > 0.001f && --maxLoops)
            {
                maxDelta = 0.0f;

                // Loop through each unpositioned Block
                for (auto blockComponent : unpositionedBlocks)
                {
                    // Store all the connections to this Block
                    Array<BlockDeviceConnection> blockConnections;
                    for (auto connection : topology.connections)
                        if (connection.device1 == blockComponent->block->uid || connection.device2 == blockComponent->block->uid)
                            blockConnections.add (connection);

                    // Loop through each connection on this Block
                    for (auto connection : blockConnections)
                    {
                        // Work out whether this Block is device 1 or device 2 in the BlockDeviceConnection struct
                        auto isDevice1 = true;
                        if (blockComponent->block->uid == connection.device2)
                            isDevice1 = false;

                        // Get the connected ports
                        auto thisPort  = isDevice1 ? connection.connectionPortOnDevice1 : connection.connectionPortOnDevice2;
                        auto otherPort = isDevice1 ? connection.connectionPortOnDevice2 : connection.connectionPortOnDevice1;

                        // Get the other Block
                        for (auto otherBlockComponent : blockComponents)
                        {
                            if (otherBlockComponent->block->uid == (isDevice1 ? connection.device2 : connection.device1))
                            {
                                // Get the rotation
                                auto rotation = getRotation (otherPort.edge, thisPort.edge) + otherBlockComponent->rotation;
                                if (rotation > 360)
                                    rotation -= 360;

                                blockComponent->rotation = rotation;

                                // Get the offsets for the connected ports
                                auto otherBlockOffset = (otherBlockComponent->topLeft + otherBlockComponent->getOffsetForPort (otherPort));
                                auto thisBlockOffset  = (blockComponent->topLeft + blockComponent->getOffsetForPort (thisPort));

                                // Work out the distance between the two connected ports
                                auto delta = otherBlockOffset - thisBlockOffset;

                                // Move this block half the distance to the connection
                                blockComponent->topLeft += delta / 2.0f;

                                // Work out whether we are close enough for the loop to end
                                maxDelta = jmax (maxDelta, std::abs (delta.x), std::abs (delta.y));
                            }
                        }
                    }
                }
            }
        }
    }

    /** Returns a rotation in degrees based on the connected edges of two blocks */
    int getRotation (Block::ConnectionPort::DeviceEdge staticEdge, Block::ConnectionPort::DeviceEdge rotatedEdge)
    {
        using edge = Block::ConnectionPort::DeviceEdge;

        switch (staticEdge)
        {
            case edge::north:
            {
                switch (rotatedEdge)
                {
                    case edge::north:
                        return 180;
                    case edge::south:
                        return 0;
                    case edge::east:
                        return 90;
                    case edge::west:
                        return 270;
                    default:
                        break;
                }

                break;
            }
            case edge::south:
            {
                switch (rotatedEdge)
                {
                    case edge::north:
                        return 0;
                    case edge::south:
                        return 180;
                    case edge::east:
                        return 270;
                    case edge::west:
                        return 90;
                    default:
                        break;
                }

                break;
            }
            case edge::east:
            {
                switch (rotatedEdge)
                {
                    case edge::north:
                        return 270;
                    case edge::south:
                        return 90;
                    case edge::east:
                        return 180;
                    case edge::west:
                        return 0;
                    default:
                        break;
                }

                break;
            }

            case edge::west:
            {
                switch (rotatedEdge)
                {
                    case edge::north:
                        return 90;
                    case edge::south:
                        return 270;
                    case edge::east:
                        return 0;
                    case edge::west:
                        return 180;
                    default:
                        break;
                }

                break;
            }

            default:
                break;
        }

        return 0;
    }

    //==============================================================================
    TooltipWindow tooltipWindow;

    PhysicalTopologySource topologySource;
    OwnedArray<BlockComponent> blockComponents;
    BlockComponent* masterBlockComponent = nullptr;

    Label noBlocksLabel;

    TextButton zoomOutButton;
    TextButton zoomInButton;

    int blockUnitInPixels;
    bool isInitialResized;

   #if JUCE_IOS
    TextButton connectButton;
   #endif

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BlocksMonitorDemo)
};
