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

   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.

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


/**
    This component runs in a client process, draws the part of the canvas that this
    particular client covers, and updates itself when messages arrive from the master
    containing new canvas states.
*/
class ClientCanvasComponent final : public Component,
                                    private OSCSender,
                                    private OSCReceiver,
                                    private OSCReceiver::Listener<OSCReceiver::RealtimeCallback>,
                                    private AsyncUpdater,
                                    private Timer
{
public:
    ClientCanvasComponent (PropertiesFile& p, int windowIndex)  : properties (p)
    {
        {
            String uuidPropName ("UUID" + String (windowIndex));
            clientName = properties.getValue (uuidPropName);

            if (clientName.isEmpty())
            {
                clientName = "CLIENT_" + String (Random().nextInt (10000)).toUpperCase();
                properties.setValue (uuidPropName, clientName);
            }
        }

        setOpaque (true);
        setSize (1500, 900);

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

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

        OSCReceiver::addListener (this);

        handleTimerCallback();
        startTimer (2000);
    }

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

private:
    void mouseDrag (const MouseEvent& e) override
    {
        auto clientArea = getAreaInGlobalSpace();

        if (! clientArea.isEmpty())
        {
            OSCMessage message (userInputOSCAddress);

            message.addString (clientName);
            message.addFloat32 (e.position.x * clientArea.getWidth()  / (float) getWidth()  + clientArea.getX());
            message.addFloat32 (e.position.y * clientArea.getHeight() / (float) getHeight() + clientArea.getY());

            send (message);
        }
    }

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

        if (address.matches (canvasStateOSCAddress))
            canvasStateOSCMessageReceived (message);
    }

    struct NewStateMessage final : public Message
    {
        NewStateMessage (const MemoryBlock& d) : data (d) {}
        MemoryBlock data;
    };

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

        if (packetiser.appendIncomingBlock (message[0].getBlob()))
        {
            const ScopedLock sl (canvasLock);

            MemoryBlock newCanvasData;

            if (packetiser.reassemble (newCanvasData))
            {
                MemoryInputStream i (newCanvasData.getData(), newCanvasData.getSize(), false);
                canvas2.load (i);
                triggerAsyncUpdate();
            }
        }
    }

    //==============================================================================
    String getMachineInfoToDisplay() const
    {
        auto* display = Desktop::getInstance().getDisplays().getDisplayForPoint (getScreenBounds().getCentre());
        return getOSName() + "   " + String (display->dpi) + "   "  + String (display->scale);
    }

    static String getOSName()
    {
       #if JUCE_MAC
        return "Mac OSX";
       #elif JUCE_ANDROID
        return "Android";
       #elif JUCE_IOS
        return "iOS";
       #elif JUCE_WINDOWS
        return "Windows";
       #elif JUCE_LINUX
        return "Linux";
       #elif JUCE_BSD
        return "BSD";
       #endif
    }

    void paint (Graphics& g) override
    {
        g.fillAll (canvas.backgroundColour);

        auto clientArea = getAreaInGlobalSpace();

        if (clientArea.isEmpty())
        {
            g.setColour (Colours::red.withAlpha (0.5f));
            g.setFont (20.0f);
            g.drawText ("Not Connected", getLocalBounds(), Justification::centred, false);
            return;
        }

        canvas.draw (g, getLocalBounds().toFloat(), clientArea);

        g.setFont (Font (34.0f));
        g.setColour (Colours::white.withAlpha (0.6f));

        g.drawText (getMachineInfoToDisplay(),
                    getLocalBounds().reduced (10).removeFromBottom (20),
                    Justification::centredRight, true);

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

    Rectangle<float> getAreaInGlobalSpace() const
    {
        if (auto client = canvas.findClient (clientName))
        {
            auto screenBounds = getScreenBounds();
            auto* display = Desktop::getInstance().getDisplays().getDisplayForPoint (screenBounds.getCentre());
            return ((screenBounds - display->userArea.getCentre()).toFloat() / (client->scaleFactor * display->dpi / display->scale)) + client->centre;
        }

        return {};
    }

    Rectangle<float> getScreenAreaInGlobalSpace() const
    {
        if (auto client = canvas.findClient (clientName))
        {
            auto* display = Desktop::getInstance().getDisplays().getDisplayForPoint (getScreenBounds().getCentre());
            return (display->userArea.toFloat() / (client->scaleFactor * display->dpi / display->scale)).withCentre (client->centre);
        }

        return {};
    }

    void handleTimerCallback()
    {
        send (newClientOSCAddress, clientName + ":" + IPAddress::getLocalAddress().toString()
                                              + ":" + getScreenAreaInGlobalSpace().toString());
    }


    void timerCallback() override
    {
        handleTimerCallback();
    }

    void handleAsyncUpdate() override
    {
        const ScopedLock sl (canvasLock);
        canvas.swapWith (canvas2);
        repaint();
    }

    SharedCanvasDescription canvas, canvas2;
    PropertiesFile& properties;
    String clientName, error;

    CriticalSection canvasLock;
    BlockPacketiser packetiser;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ClientCanvasComponent)
};
