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

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

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 7 End-User License
   Agreement and JUCE Privacy Policy.

   End User License Agreement: www.juce.com/juce-7-licence
   Privacy Policy: www.juce.com/juce-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

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

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


/**
    Runs the master node, calls the demo to update the canvas, broadcasts those changes
    out to slaves, and shows a view of all the clients to allow them to be dragged around.
*/
struct MasterContentComponent  : public Component,
                                 private Timer,
                                 private OSCSender,
                                 private OSCReceiver,
                                 private OSCReceiver::Listener<OSCReceiver::MessageLoopCallback>
{
    MasterContentComponent (PropertiesFile& props)
        : properties (props)
    {
        setWantsKeyboardFocus (true);
        createAllDemos (demos);
        setContent (0);

        setSize ((int) (15.0f * currentCanvas.getLimits().getWidth()),
                 (int) (15.0f * currentCanvas.getLimits().getHeight()));

        if (! OSCSender::connect (getBroadcastIPAddress(), masterPortNumber))
            error = "Master app OSC sender: network connection error.";

        if (! OSCReceiver::connect (clientPortNumber))
            error = "Master app OSC receiver: network connection error.";

        OSCReceiver::addListener (this);

        startTimerHz (30);
    }

    ~MasterContentComponent() override
    {
        OSCReceiver::removeListener (this);
    }

    //==============================================================================
    struct Client
    {
        String name, ipAddress;
        float widthInches, heightInches;
        Point<float> centre; // in inches
        float scaleFactor;
    };

    Array<Client> clients;

    void addClient (String name, String ipAddress, String areaDescription)
    {
        auto area = Rectangle<float>::fromString (areaDescription);

        if (auto c = getClient (name))
        {
            c->ipAddress = ipAddress;
            c->widthInches = area.getWidth();
            c->heightInches = area.getHeight();
            return;
        }

        DBG (name + "   "  + ipAddress);

        removeClient (name);
        clients.add ({ name, ipAddress, area.getWidth(), area.getHeight(), {}, 1.0f });

        String lastX = properties.getValue ("lastX_" + name);
        String lastY = properties.getValue ("lastY_" + name);
        String lastScale = properties.getValue ("scale_" + name);

        if (lastX.isEmpty() || lastY.isEmpty())
            setClientCentre (name, { Random().nextFloat() * 10.0f,
                                     Random().nextFloat() * 10.0f });
        else
            setClientCentre (name, Point<float> (lastX.getFloatValue(),
                                                 lastY.getFloatValue()));

        if (lastScale.isNotEmpty())
            setClientScale (name, lastScale.getFloatValue());
        else
            setClientScale (name, 1.0f);

        updateDeviceComponents();
    }

    void removeClient (String name)
    {
        for (int i = clients.size(); --i >= 0;)
            if (clients.getReference (0).name == name)
                clients.remove (i);

        updateDeviceComponents();
    }

    void setClientCentre (const String& name, Point<float> newCentre)
    {
        if (auto c = getClient (name))
        {
            newCentre = currentCanvas.getLimits().getConstrainedPoint (newCentre);
            c->centre = newCentre;

            properties.setValue ("lastX_" + name, String (newCentre.x));
            properties.setValue ("lastY_" + name, String (newCentre.y));

            startTimer (1);
        }
    }

    float getClientScale (const String& name) const
    {
        if (auto c = getClient (name))
            return c->scaleFactor;

        return 1.0f;
    }

    void setClientScale (const String& name, float newScale)
    {
        if (auto c = getClient (name))
        {
            c->scaleFactor = jlimit (0.5f, 2.0f, newScale);
            properties.setValue ("scale_" + name, String (newScale));
        }
    }

    Point<float> getClientCentre (const String& name) const
    {
        if (auto c = getClient (name))
            return c->centre;

        return {};
    }

    Rectangle<float> getClientArea (const String& name) const
    {
        if (auto c = getClient (name))
            return Rectangle<float> (c->widthInches, c->heightInches)
                     .withCentre (c->centre);

        return {};
    }

    Rectangle<float> getActiveCanvasArea() const
    {
        Rectangle<float> r;

        if (clients.size() > 0)
            r = Rectangle<float> (1.0f, 1.0f).withCentre (clients.getReference (0).centre);

        for (int i = 1; i < clients.size(); ++i)
            r = r.getUnion (Rectangle<float> (1.0f, 1.0f).withCentre (clients.getReference (i).centre));

        return r.expanded (6.0f);
    }

    int getContentIndex() const
    {
        return demos.indexOf (content);
    }

    void setContent (int demoIndex)
    {
        content = demos[demoIndex];

        if (content != nullptr)
            content->reset();
    }

    bool keyPressed (const KeyPress& key) override
    {
        if (key == KeyPress::spaceKey || key == KeyPress::rightKey || key == KeyPress::downKey)
        {
            setContent ((getContentIndex() + 1) % demos.size());
            return true;
        }

        if (key == KeyPress::upKey || key == KeyPress::leftKey)
        {
            setContent ((getContentIndex() + demos.size() - 1) % demos.size());
            return true;
        }

        return Component::keyPressed (key);
    }

private:
    //==============================================================================
    void paint (Graphics& g) override
    {
        g.fillAll (Colours::black);

        currentCanvas.draw (g, getLocalBounds().toFloat(), currentCanvas.getLimits());

        if (error.isNotEmpty())
        {
            g.setColour (Colours::red);
            g.setFont (20.0f);
            g.drawText (error, getLocalBounds().reduced (10).removeFromBottom (80),
                        Justification::centredRight, true);
        }

        if (content != nullptr)
        {
            g.setColour (Colours::white);
            g.setFont (17.0f);
            g.drawText ("Demo: " + content->getName(),
                        getLocalBounds().reduced (10).removeFromTop (30),
                        Justification::centredLeft, true);
        }
    }

    void resized() override
    {
        updateDeviceComponents();
    }

    void updateDeviceComponents()
    {
        for (int i = devices.size(); --i >= 0;)
            if (getClient (devices.getUnchecked(i)->getName()) == nullptr)
                devices.remove (i);

        for (const auto& c : clients)
            if (getDeviceComponent (c.name) == nullptr)
                addAndMakeVisible (devices.add (new DeviceComponent (*this, c.name)));

        for (auto d : devices)
            d->setBounds (virtualSpaceToLocal (getClientArea (d->getName())).getSmallestIntegerContainer());
    }

    Point<float> virtualSpaceToLocal (Point<float> p) const
    {
        auto total = currentCanvas.getLimits();

        return { (float) getWidth()  * (p.x - total.getX()) / total.getWidth(),
                 (float) getHeight() * (p.y - total.getY()) / total.getHeight() };
    }

    Rectangle<float> virtualSpaceToLocal (Rectangle<float> p) const
    {
        return { virtualSpaceToLocal (p.getTopLeft()),
                 virtualSpaceToLocal (p.getBottomRight()) };
    }

    Point<float> localSpaceToVirtual (Point<float> p) const
    {
        auto total = currentCanvas.getLimits();

        return { total.getX() + total.getWidth()  * (p.x / (float) getWidth()),
                 total.getY() + total.getHeight() * (p.y / (float) getHeight()) };
    }

    //==============================================================================
    struct DeviceComponent  : public Component
    {
        DeviceComponent (MasterContentComponent& e, String name)
            : Component (name), editor (e)
        {
            setMouseCursor (MouseCursor::DraggingHandCursor);
        }

        void paint (Graphics& g) override
        {
            g.fillAll (Colours::blue.withAlpha (0.4f));

            g.setColour (Colours::white);
            g.setFont (11.0f);
            g.drawFittedText (getName(), getLocalBounds(), Justification::centred, 2);
        }

        void mouseDown (const MouseEvent&) override
        {
            dragStartLocation = editor.getClientCentre (getName());
        }

        void mouseDrag (const MouseEvent& e) override
        {
            editor.setClientCentre (getName(), dragStartLocation
                                                + editor.localSpaceToVirtual (e.getPosition().toFloat())
                                                - editor.localSpaceToVirtual (e.getMouseDownPosition().toFloat()));
        }

        void mouseWheelMove (const MouseEvent&, const MouseWheelDetails& e) override
        {
            editor.setClientScale (getName(), editor.getClientScale (getName()) + 0.1f * e.deltaY);
        }

        void mouseDoubleClick (const MouseEvent&) override
        {
            editor.setClientScale (getName(), 1.0f);
        }

        MasterContentComponent& editor;
        Point<float> dragStartLocation;
        Rectangle<float> clientArea;
    };

    DeviceComponent* getDeviceComponent (const String& name) const
    {
        for (auto d : devices)
            if (d->getName() == name)
                return d;

        return nullptr;
    }

    //==============================================================================
    void broadcastNewCanvasState (const MemoryBlock& canvasData)
    {
        BlockPacketiser packetiser;
        packetiser.createBlocksFromData (canvasData, 1000);

        for (const auto& client : clients)
            for (auto& b : packetiser.blocks)
                sendToIPAddress (client.ipAddress, masterPortNumber, canvasStateOSCAddress, b);
    }

    void timerCallback() override
    {
        startTimerHz (30);

        currentCanvas.reset();
        updateCanvasInfo (currentCanvas);

        {
            std::unique_ptr<CanvasGeneratingContext> context (new CanvasGeneratingContext (currentCanvas));
            Graphics g (*context);

            if (content != nullptr)
                content->generateCanvas (g, currentCanvas, getActiveCanvasArea());
        }

        broadcastNewCanvasState (currentCanvas.toMemoryBlock());

        updateDeviceComponents();
        repaint();
    }

    void updateCanvasInfo (SharedCanvasDescription& canvas)
    {
        canvas.backgroundColour = Colours::black;

        for (const auto& c : clients)
            canvas.clients.add ({ c.name, c.centre, c.scaleFactor });
    }

    const Client* getClient (const String& name) const
    {
        for (auto& c : clients)
            if (c.name == name)
                return &c;

        return nullptr;
    }

    Client* getClient (const String& name)
    {
        return const_cast<Client*> (static_cast<const MasterContentComponent&> (*this).getClient (name));
    }

    //==============================================================================
    void oscMessageReceived (const OSCMessage& message) override
    {
        auto address = message.getAddressPattern();

        if (address.matches (newClientOSCAddress))       newClientOSCMessageReceived (message);
        else if (address.matches (userInputOSCAddress))  userInputOSCMessageReceived (message);
    }

    void newClientOSCMessageReceived (const OSCMessage& message)
    {
        if (message.isEmpty() || ! message[0].isString())
            return;

        StringArray tokens = StringArray::fromTokens (message[0].getString(), ":", "");
        addClient (tokens[0], tokens[1], tokens[2]);
    }

    void userInputOSCMessageReceived (const OSCMessage& message)
    {
        if (message.size() == 3 && message[0].isString() && message[1].isFloat32() && message[2].isFloat32())
        {
            content->handleTouch ({ message[1].getFloat32(),
                                    message[2].getFloat32() });
        }
    }

    //==============================================================================
    AnimatedContent* content = nullptr;
    PropertiesFile& properties;
    OwnedArray<DeviceComponent> devices;
    SharedCanvasDescription currentCanvas;
    String error;

    OwnedArray<AnimatedContent> demos;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MasterContentComponent)
};
