/*
 ==============================================================================
 This file is part of the IEM plug-in suite.
 Author: Daniel Rudrich
 Copyright (c) 2018 - Institute of Electronic Music and Acoustics (IEM)
 https://iem.at

 The IEM plug-in suite is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 The IEM plug-in suite is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this software.  If not, see <http://www.gnu.org/licenses/>.
 ==============================================================================
 */

#include "OSCStatus.h"

OSCDialogWindow::OSCDialogWindow (OSCParameterInterface& oscInterface,
                                  OSCReceiverPlus& oscReceiver,
                                  OSCSenderPlus& oscSender) :
    interface (oscInterface), receiver (oscReceiver), sender (oscSender)
{
    //==== Receiver =====================================
    isReceiverConnected = receiver.isConnected();

    addAndMakeVisible (receiverGroup);
    receiverGroup.setText ("OSC Receiver");

    addAndMakeVisible (slRecPort);
    slRecPort.setText ("Listen to port", false, juce::Justification::centred);

    addAndMakeVisible (lbRPort);
    const int receiverPort = receiver.getPortNumber();
    lbRPort.setText (receiverPort == -1 ? "none" : juce::String (receiverPort),
                     juce::NotificationType::dontSendNotification);
    lbRPort.setEditable (true);
    lbRPort.setJustificationType (juce::Justification::centred);
    lbRPort.addListener (this);

    addAndMakeVisible (tbReceiverOpen);
    tbReceiverOpen.setButtonText (isReceiverConnected ? "CLOSE" : "OPEN");
    tbReceiverOpen.setColour (juce::TextButton::buttonColourId,
                              isReceiverConnected ? juce::Colours::orangered
                                                  : juce::Colours::limegreen);
    tbReceiverOpen.onClick = [this]() { checkPortAndConnectReceiver(); };

    //==== Receiver =====================================
    isSenderConnected = sender.isConnected();

    addAndMakeVisible (senderGroup);
    senderGroup.setText ("OSC Sender");

    addAndMakeVisible (lbSPort);
    const int senderPort = sender.getPortNumber();
    lbSPort.setText (senderPort == -1 ? "none" : juce::String (senderPort),
                     juce::NotificationType::dontSendNotification);
    lbSPort.setEditable (true);
    lbSPort.setJustificationType (juce::Justification::centred);
    lbSPort.addListener (this);

    addAndMakeVisible (lbSHostname);
    const auto senderHostName = sender.getHostName();
    lbSHostname.setText (senderHostName, juce::NotificationType::dontSendNotification);
    lbSHostname.setEditable (true);
    lbSHostname.setJustificationType (juce::Justification::centred);
    lbSHostname.addListener (this);

    addAndMakeVisible (lbSOSCAddress);
    const auto senderOSCAddress = interface.getOSCAddress();
    lbSOSCAddress.setText (senderOSCAddress, juce::NotificationType::dontSendNotification);
    lbSOSCAddress.setEditable (true);
    lbSOSCAddress.setJustificationType (juce::Justification::centred);
    lbSOSCAddress.onTextChange = [this]() { updateOSCAddress(); };

    addAndMakeVisible (tbSenderOpen);
    tbSenderOpen.setButtonText (isSenderConnected ? "DISCONNECT" : "CONNECT");
    tbSenderOpen.setColour (juce::TextButton::buttonColourId,
                            isSenderConnected ? juce::Colours::orangered
                                              : juce::Colours::limegreen);
    tbSenderOpen.onClick = [this]() { checkPortAndConnectSender(); };

    addAndMakeVisible (slSendIP);
    slSendIP.setText ("IP", false, juce::Justification::centred);

    addAndMakeVisible (slSendPort);
    slSendPort.setText ("Port", false, juce::Justification::centred);

    addAndMakeVisible (slSendName);
    slSendName.setText ("OSC Addr.", false, juce::Justification::centred);

    addAndMakeVisible (tbFlush);
    tbFlush.setButtonText ("Flush Params");
    tbFlush.setColour (juce::TextButton::buttonColourId, juce::Colours::cornflowerblue);
    tbFlush.onClick = [this]() { interface.sendParameterChanges (true); };

    addAndMakeVisible (intervalSlider);
    intervalSlider.setRange (1, 1000, 1);
    intervalSlider.setValue (interface.getInterval());
    intervalSlider.setSliderStyle (juce::Slider::RotaryVerticalDrag);
    intervalSlider.setColour (juce::Slider::rotarySliderOutlineColourId,
                              juce::Colours::cornflowerblue);
    intervalSlider.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 60, 14);
    intervalSlider.setTextValueSuffix (" ms");
    intervalSlider.onValueChange = [&]() { interface.setInterval (intervalSlider.getValue()); };

    addAndMakeVisible (slInterval);
    slInterval.setText ("Interval");

    startTimer (500);
}

void OSCDialogWindow::timerCallback()
{
    bool shouldReceiverBeConnected = receiver.isConnected();
    if (isReceiverConnected != shouldReceiverBeConnected)
    {
        isReceiverConnected = shouldReceiverBeConnected;
        tbReceiverOpen.setButtonText (isReceiverConnected ? "CLOSE" : "OPEN");
        tbReceiverOpen.setColour (juce::TextButton::buttonColourId,
                                  isReceiverConnected ? juce::Colours::orangered
                                                      : juce::Colours::limegreen);
        repaint();
    }

    bool shouldSenderBeConnected = sender.isConnected();
    if (isSenderConnected != shouldSenderBeConnected)
    {
        isSenderConnected = shouldSenderBeConnected;
        tbSenderOpen.setButtonText (isSenderConnected ? "DISCONNECT" : "CONNECT");
        tbSenderOpen.setColour (juce::TextButton::buttonColourId,
                                isSenderConnected ? juce::Colours::orangered
                                                  : juce::Colours::limegreen);
        repaint();
    }
}

void OSCDialogWindow::updateOSCAddress()
{
    interface.setOSCAddress (lbSOSCAddress.getText());

    lbSOSCAddress.setText (interface.getOSCAddress(), juce::NotificationType::dontSendNotification);
}

void OSCDialogWindow::labelTextChanged (juce::Label* labelThatHasChanged)
{
    if (labelThatHasChanged == &lbRPort)
    {
        DBG ("Receiver label changed");
        auto val = lbRPort.getTextValue();
        const int v = val.getValue();

        if (receiver.isConnected())
        {
            if (v == -1 || (v > 1000 && v < 15000))
            {
                receiver.disconnect();
                checkPortAndConnectReceiver();
            }
        }
    }
    else if (labelThatHasChanged == &lbSPort || labelThatHasChanged == &lbSHostname)
    {
        DBG ("Sender label changed");
        if (sender.isConnected())
        {
            sender.disconnect();
            checkPortAndConnectSender();
        }
    }
}

void OSCDialogWindow::checkPortAndConnectSender()
{
    if (sender.isConnected())
    {
        sender.disconnect();
    }
    else
    {
        if (lbSPort.getText() == "none" || lbSPort.getText() == "off")
        {
            sender.connect ("", -1);
            lbSPort.setText ("none", juce::NotificationType::dontSendNotification);
        }

        auto val = lbSPort.getTextValue();
        const int v = val.getValue();

        const auto ip = lbSHostname.getText();

        if (v == -1 || (v > 1000 && v < 15000))
        {
            if (! sender.connect (ip, v))
            {
                juce::AlertWindow alert (
                    "Connection could not be established!",
                    "Make sure the desired port is available and not already occupied by other clients. Also make sure the IP has a correct format!",
                    juce::AlertWindow::NoIcon,
                    this);
                alert.setLookAndFeel (&getLookAndFeel());
                alert.addButton ("OK", 1, juce::KeyPress (juce::KeyPress::returnKey, 0, 0));
                alert.runModalLoop();
            }
        }
    }
}

void OSCDialogWindow::checkPortAndConnectReceiver()
{
    if (receiver.isConnected())
    {
        receiver.disconnect();
    }
    else
    {
        if (lbRPort.getText() == "none" || lbRPort.getText() == "off")
        {
            receiver.connect (-1);
            lbRPort.setText ("none", juce::NotificationType::dontSendNotification);
        }

        auto val = lbRPort.getTextValue();
        int v = val.getValue();

        if (v == -1 || (v > 1000 && v < 15000))
        {
            if (! receiver.connect (v))
            {
                juce::AlertWindow alert (
                    "Connection could not be established!",
                    "Make sure the desired port is available and not already occupied by other clients.",
                    juce::AlertWindow::NoIcon,
                    this);
                alert.setLookAndFeel (&getLookAndFeel());
                alert.addButton ("OK", 1, juce::KeyPress (juce::KeyPress::returnKey, 0, 0));
                alert.runModalLoop();
            }
        }
    }
}

void OSCDialogWindow::resized()
{
    auto bounds = getLocalBounds();

    //==== Receiver =================
    receiverGroup.setBounds (bounds.removeFromTop (25));

    auto row = bounds.removeFromTop (20);
    slRecPort.setBounds (row.removeFromLeft (80));
    row.removeFromLeft (3);
    lbRPort.setBounds (row.removeFromLeft (50));
    row.removeFromLeft (8);
    tbReceiverOpen.setBounds (row);

    bounds.removeFromTop (10);

    //==== Sender =================
    senderGroup.setBounds (bounds.removeFromTop (25));

    row = bounds.removeFromTop (20);
    slSendIP.setBounds (row.removeFromLeft (40));
    row.removeFromLeft (3);
    lbSHostname.setBounds (row);

    bounds.removeFromTop (5);
    row = bounds.removeFromTop (20);

    slSendPort.setBounds (row.removeFromLeft (40));
    row.removeFromLeft (3);
    lbSPort.setBounds (row.removeFromLeft (50));
    row.removeFromLeft (8);
    tbSenderOpen.setBounds (row);

    bounds.removeFromTop (5);
    row = bounds.removeFromTop (20);

    slSendName.setBounds (row.removeFromLeft (60));
    row.removeFromLeft (1);
    lbSOSCAddress.setBounds (row);

    bounds.removeFromTop (5);

    row = bounds.removeFromTop (50);
    slInterval.setBounds (row.removeFromLeft (40));
    row.removeFromLeft (3);
    intervalSlider.setBounds (row.removeFromLeft (60));

    row.reduce (0, 15);
    tbFlush.setBounds (row.removeFromRight (80));
}

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

OSCStatus::OSCStatus (OSCParameterInterface& oscInterface) :
    oscParameterInterface (oscInterface),
    oscReceiver (oscInterface.getOSCReceiver()),
    oscSender (oscInterface.getOSCSender())
{
    isReceiverOpen = oscReceiver.isConnected();
    startTimer (500);
}

void OSCStatus::timerCallback()
{
    const int receiverPort = oscReceiver.getPortNumber();
    const int senderPort = oscSender.getPortNumber();
    const juce::String senderHostName = oscSender.getHostName();

    bool shouldReceiverBeConnected = oscReceiver.isConnected();
    bool shouldSenderBeConnected = oscSender.isConnected();

    if (isReceiverOpen != shouldReceiverBeConnected || lastReceiverPort != receiverPort)
    {
        lastReceiverPort = receiverPort;
        isReceiverOpen = shouldReceiverBeConnected;
        repaint();
    }

    if (isSenderOpen != shouldSenderBeConnected || lastSenderPort != senderPort
        || lastSenderHostName != senderHostName)
    {
        lastSenderPort = senderPort;
        lastSenderHostName = senderHostName;
        isSenderOpen = shouldSenderBeConnected;
        repaint();
    }
}

void OSCStatus::mouseMove (const juce::MouseEvent& event)
{
    if (! mouseOver && bounds.contains (event.getPosition()))
    {
        setMouseCursor (juce::MouseCursor::PointingHandCursor);
        mouseOver = true;
        repaint();
    }
    else if (mouseOver && ! bounds.contains (event.getPosition()))
    {
        setMouseCursor (juce::MouseCursor::NormalCursor);
        mouseOver = false;
        repaint();
    }
}

void OSCStatus::mouseExit (const juce::MouseEvent& event)
{
    ignoreUnused (event);
    mouseOver = false;
    repaint();
}

void OSCStatus::mouseUp (const juce::MouseEvent& event)
{
    if (bounds.contains (event.getPosition()))
    {
        auto dialogWindow =
            std::make_unique<OSCDialogWindow> (oscParameterInterface, oscReceiver, oscSender);
        dialogWindow->setSize (211, 210);

        juce::CallOutBox& myBox = juce::CallOutBox::launchAsynchronously (
            std::move (dialogWindow),
            getScreenBounds().removeFromLeft (bounds.getWidth()),
            nullptr);
        myBox.setLookAndFeel (&getLookAndFeel());
    }
}

void OSCStatus::paint (juce::Graphics& g)
{
    juce::Colour receiveStatusColor =
        oscReceiver.getPortNumber() == -1 ? juce::Colours::white.withAlpha (0.1f)
        : oscReceiver.isConnected()       ? juce::Colours::limegreen
                                          : juce::Colours::red.withAlpha (0.5f);
    juce::Colour sendStatusColor = oscSender.getPortNumber() == -1
                                       ? juce::Colours::white.withAlpha (0.1f)
                                   : oscSender.isConnected() ? juce::Colours::limegreen
                                                             : juce::Colours::red.withAlpha (0.5f);

    const float alpha = mouseOver ? 1.0f : 0.5f;

    auto area = getLocalBounds();
    area = area.removeFromBottom (12);

    auto circleArea = area.removeFromLeft (12).toFloat().reduced (2.0f);
    circleArea.setY (circleArea.getY() - 1.0f);
    g.setColour (receiveStatusColor.withAlpha (alpha));
    g.drawRoundedRectangle (circleArea, 6, 1.0f);
    g.setColour (receiveStatusColor);
    g.fillRoundedRectangle (circleArea.removeFromLeft (14).reduced (2.0f), 6);

    area.removeFromLeft (2);

    circleArea = area.removeFromLeft (12).toFloat().reduced (2.0f);
    circleArea.setY (circleArea.getY() - 1.0f);
    g.setColour (sendStatusColor.withAlpha (alpha));
    g.drawRoundedRectangle (circleArea, 6, 1.0f);
    g.setColour (sendStatusColor);
    g.fillRoundedRectangle (circleArea.removeFromLeft (14).reduced (2.0f), 6);

    area.removeFromLeft (2);

    g.setColour (juce::Colours::white.withAlpha (mouseOver ? 1.0f : 0.5f));
    auto currentFont =
        juce::FontOptions (getLookAndFeel().getTypefaceForFont (juce::FontOptions (14.0f, 0)))
            .withHeight (14.0f);
    g.setFont (currentFont);

    juce::AttributedString text;
    text.setText ("OSC");
    if (oscReceiver.isConnected() || oscSender.isConnected())
    {
        text.append (" (");
        if (oscReceiver.isConnected())
            text.append ("IN: " + juce::String (oscReceiver.getPortNumber()));

        if (oscReceiver.isConnected() && oscSender.isConnected())
            text.append (" - ");

        if (oscSender.isConnected())
            text.append ("OUT: " + oscSender.getHostName() + ":"
                         + juce::String (oscSender.getPortNumber()));

        text.append (")");
    }
    text.setFont (currentFont);
    text.setColour (juce::Colours::white.withAlpha (mouseOver ? 1.0f : 0.5f));
    text.setJustification (juce::Justification::bottomLeft);

    auto maxWidth = area.getWidth();
    juce::TextLayout layout;
    layout.createLayout (text, maxWidth);
    auto textWidth = static_cast<int> (std::ceil (layout.getWidth()));

    const int targetSize = 12 + 2 + textWidth + 2 + 12;

    bounds = getLocalBounds().removeFromLeft (targetSize);

    juce::Rectangle<float> areaFl = area.toFloat();
    layout.draw (g, area.toFloat().withWidth (textWidth));
}
