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

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

   JUCE is an open source framework subject to commercial or open source
   licensing.

   By downloading, installing, or using the JUCE framework, or combining the
   JUCE framework with any other source code, object code, content or any other
   copyrightable work, you agree to the terms of the JUCE End User Licence
   Agreement, and all incorporated terms including the JUCE Privacy Policy and
   the JUCE Website Terms of Service, as applicable, which will bind you. If you
   do not agree to the terms of these agreements, we will not license the JUCE
   framework to you, and you must discontinue the installation or download
   process and cease use of the JUCE framework.

   JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
   JUCE Privacy Policy: https://juce.com/juce-privacy-policy
   JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/

   Or:

   You may also use this code under the terms of the AGPLv3:
   https://www.gnu.org/licenses/agpl-3.0.en.html

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

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

namespace juce
{

namespace
{
    //==============================================================================
    /** Writes OSC data to an internal memory buffer, which grows as required.

        The data that was written into the stream can then be accessed later as
        a contiguous block of memory.

        This class implements the Open Sound Control 1.0 Specification for
        the format in which the OSC data will be written into the buffer.
    */
    struct OSCOutputStream
    {
        OSCOutputStream() noexcept {}

        /** Returns a pointer to the data that has been written to the stream. */
        const void* getData() const noexcept    { return output.getData(); }

        /** Returns the number of bytes of data that have been written to the stream. */
        size_t getDataSize() const noexcept     { return output.getDataSize(); }

        //==============================================================================
        bool writeInt32 (int32 value)
        {
            return output.writeIntBigEndian (value);
        }

        bool writeUint64 (uint64 value)
        {
            return output.writeInt64BigEndian (int64 (value));
        }

        bool writeFloat32 (float value)
        {
            return output.writeFloatBigEndian (value);
        }

        bool writeString (const String& value)
        {
            if (! output.writeString (value))
                return false;

            const size_t numPaddingZeros = ~value.getNumBytesAsUTF8() & 3;

            return output.writeRepeatedByte ('\0', numPaddingZeros);
        }

        bool writeBlob (const MemoryBlock& blob)
        {
            if (! (output.writeIntBigEndian ((int) blob.getSize())
                    && output.write (blob.getData(), blob.getSize())))
                return false;

            const size_t numPaddingZeros = ~(blob.getSize() - 1) & 3;

            return output.writeRepeatedByte (0, numPaddingZeros);
        }

        bool writeColour (OSCColour colour)
        {
            return output.writeIntBigEndian ((int32) colour.toInt32());
        }

        bool writeTimeTag (OSCTimeTag timeTag)
        {
            return output.writeInt64BigEndian (int64 (timeTag.getRawTimeTag()));
        }

        bool writeAddress (const OSCAddress& address)
        {
            return writeString (address.toString());
        }

        bool writeAddressPattern (const OSCAddressPattern& ap)
        {
            return writeString (ap.toString());
        }

        bool writeTypeTagString (const OSCTypeList& typeList)
        {
            output.writeByte (',');

            if (typeList.size() > 0)
                output.write (typeList.begin(), (size_t) typeList.size());

            output.writeByte ('\0');

            size_t bytesWritten = (size_t) typeList.size() + 1;
            size_t numPaddingZeros = ~bytesWritten & 0x03;

            return output.writeRepeatedByte ('\0', numPaddingZeros);
        }

        bool writeArgument (const OSCArgument& arg)
        {
            switch (arg.getType())
            {
                case OSCTypes::int32:       return writeInt32 (arg.getInt32());
                case OSCTypes::float32:     return writeFloat32 (arg.getFloat32());
                case OSCTypes::string:      return writeString (arg.getString());
                case OSCTypes::blob:        return writeBlob (arg.getBlob());
                case OSCTypes::colour:      return writeColour (arg.getColour());

                default:
                    // In this very unlikely case you supplied an invalid OSCType!
                    jassertfalse;
                    return false;
            }
        }

        //==============================================================================
        bool writeMessage (const OSCMessage& msg)
        {
            if (! writeAddressPattern (msg.getAddressPattern()))
                return false;

            OSCTypeList typeList;

            for (auto& arg : msg)
                typeList.add (arg.getType());

            if (! writeTypeTagString (typeList))
                return false;

            for (auto& arg : msg)
                if (! writeArgument (arg))
                    return false;

            return true;
        }

        bool writeBundle (const OSCBundle& bundle)
        {
            if (! writeString ("#bundle"))
                return false;

            if (! writeTimeTag (bundle.getTimeTag()))
                return false;

            for (auto& element : bundle)
                if (! writeBundleElement (element))
                    return false;

            return true;
        }

        //==============================================================================
        bool writeBundleElement (const OSCBundle::Element& element)
        {
            const int64 startPos = output.getPosition();

            if (! writeInt32 (0))   // writing dummy value for element size
                return false;

            if (element.isBundle())
            {
                if (! writeBundle (element.getBundle()))
                    return false;
            }
            else
            {
                if (! writeMessage (element.getMessage()))
                    return false;
            }

            const int64 endPos = output.getPosition();
            const int64 elementSize = endPos - (startPos + 4);

            return output.setPosition (startPos)
                     && writeInt32 ((int32) elementSize)
                     && output.setPosition (endPos);
        }

    private:
        MemoryOutputStream output;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OSCOutputStream)
    };

} // namespace


//==============================================================================
struct OSCSender::Pimpl
{
    Pimpl() noexcept  {}
    ~Pimpl() noexcept { disconnect(); }

    //==============================================================================
    bool connect (const String& newTargetHost, int newTargetPort)
    {
        if (! disconnect())
            return false;

        socket.setOwned (new DatagramSocket (true));
        targetHostName = newTargetHost;
        targetPortNumber = newTargetPort;

        if (socket->bindToPort (0)) // 0 = use any local port assigned by the OS.
            return true;

        socket.reset();
        return false;
    }

    bool connectToSocket (DatagramSocket& newSocket, const String& newTargetHost, int newTargetPort)
    {
        if (! disconnect())
            return false;

        socket.setNonOwned (&newSocket);
        targetHostName = newTargetHost;
        targetPortNumber = newTargetPort;
        return true;
    }

    bool disconnect()
    {
        socket.reset();
        return true;
    }

    //==============================================================================
    bool send (const OSCMessage& message, const String& hostName, int portNumber)
    {
        OSCOutputStream outStream;

        return outStream.writeMessage (message)
            && sendOutputStream (outStream, hostName, portNumber);
    }

    bool send (const OSCBundle& bundle, const String& hostName, int portNumber)
    {
        OSCOutputStream outStream;

        return outStream.writeBundle (bundle)
            && sendOutputStream (outStream, hostName, portNumber);
    }

    bool send (const OSCMessage& message)   { return send (message, targetHostName, targetPortNumber); }
    bool send (const OSCBundle& bundle)     { return send (bundle,  targetHostName, targetPortNumber); }

private:
    //==============================================================================
    bool sendOutputStream (OSCOutputStream& outStream, const String& hostName, int portNumber)
    {
        if (socket != nullptr)
        {
            const int streamSize = (int) outStream.getDataSize();

            const int bytesWritten = socket->write (hostName, portNumber,
                                                    outStream.getData(), streamSize);
            return bytesWritten == streamSize;
        }

        // if you hit this, you tried to send some OSC data without being
        // connected to a port! You should call OSCSender::connect() first.
        jassertfalse;

        return false;
    }

    //==============================================================================
    OptionalScopedPointer<DatagramSocket> socket;
    String targetHostName;
    int targetPortNumber = 0;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
};


//==============================================================================
OSCSender::OSCSender()   : pimpl (new Pimpl())
{
}

OSCSender::~OSCSender()
{
    pimpl->disconnect();
    pimpl.reset();
}

//==============================================================================
bool OSCSender::connect (const String& targetHostName, int targetPortNumber)
{
    return pimpl->connect (targetHostName, targetPortNumber);
}

bool OSCSender::connectToSocket (DatagramSocket& socket, const String& targetHostName, int targetPortNumber)
{
    return pimpl->connectToSocket (socket, targetHostName, targetPortNumber);
}

bool OSCSender::disconnect()
{
    return pimpl->disconnect();
}

//==============================================================================
bool OSCSender::send (const OSCMessage& message)    { return pimpl->send (message); }
bool OSCSender::send (const OSCBundle& bundle)      { return pimpl->send (bundle); }

bool OSCSender::sendToIPAddress (const String& host, int port, const OSCMessage& message) { return pimpl->send (message, host, port); }
bool OSCSender::sendToIPAddress (const String& host, int port, const OSCBundle& bundle)   { return pimpl->send (bundle,  host, port); }


//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS

class OSCBinaryWriterTests final : public UnitTest
{
public:
    OSCBinaryWriterTests()
        : UnitTest ("OSCBinaryWriter class", UnitTestCategories::osc)
    {}

    void runTest() override
    {
        beginTest ("writing OSC addresses");
        {
            OSCOutputStream outStream;
            const char check[16] = { '/', 't', 'e', 's', 't', '/', 'f', 'a', 'd', 'e', 'r', '7', '\0', '\0', '\0', '\0' };

            OSCAddress address ("/test/fader7");
            expect (outStream.writeAddress (address));

            expect (outStream.getDataSize() == sizeof (check));
            expect (std::memcmp (outStream.getData(), check, sizeof (check)) == 0);
        }

        beginTest ("writing OSC address patterns");
        {
            OSCOutputStream outStream;
            const char check[20] = { '/', '*', '/', '*', 'p', 'u', 't', '/', 'f', 'a', 'd', 'e', 'r', '[', '0', '-', '9', ']', '\0', '\0' };

            OSCAddressPattern ap ("/*/*put/fader[0-9]");
            expect (outStream.writeAddressPattern (ap));

            expect (outStream.getDataSize() == sizeof (check));
            expect (std::memcmp (outStream.getData(), check, sizeof (check)) == 0);
        }

        beginTest ("writing OSC time tags");
        {
            OSCOutputStream outStream;
            const char check[8] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 };
            OSCTimeTag tag;

            expect (outStream.writeTimeTag (tag));
            expect (outStream.getDataSize() == 8);
            expect (std::memcmp (outStream.getData(), check, sizeof (check)) == 0);
        }

        beginTest ("writing OSC type tag strings");
        {
            {
                OSCOutputStream outStream;

                OSCTypeList list;

                const char check[4] = { ',', '\0', '\0', '\0' };
                expect (outStream.writeTypeTagString (list));
                expect (outStream.getDataSize() == 4);
                expect (std::memcmp (outStream.getData(), check, sizeof (check)) == 0);
            }

            {
                OSCOutputStream outStream;

                OSCTypeList list;
                list.add (OSCTypes::int32);
                list.add (OSCTypes::float32);

                const char check[4] = { ',', 'i', 'f', '\0' };
                expect (outStream.writeTypeTagString (list));
                expect (outStream.getDataSize() == sizeof (check));
                expect (std::memcmp (outStream.getData(), check, sizeof (check)) == 0);
            }

            {
                OSCOutputStream outStream;

                OSCTypeList list;
                list.add (OSCTypes::blob);
                list.add (OSCTypes::blob);
                list.add (OSCTypes::string);

                const char check[8] = { ',', 'b', 'b', 's', '\0', '\0', '\0', '\0' };
                expect (outStream.writeTypeTagString (list));
                expect (outStream.getDataSize() == sizeof (check));
                expect (std::memcmp (outStream.getData(), check, sizeof (check)) == 0);
            }
        }

        beginTest ("writing OSC arguments");
        {
            // test data:
            int testInt = -2015;
            const uint8 testIntRepresentation[] =  { 0xFF, 0xFF, 0xF8, 0x21 }; // big endian two's complement

            float testFloat = 345.6125f;
            const uint8 testFloatRepresentation[] = { 0x43, 0xAC, 0xCE, 0x66 }; // big endian IEEE 754

            String testString = "Hello, World!";
            const char testStringRepresentation[] = { 'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\0', '\0', '\0' }; // padded to size % 4 == 0

            const uint8 testBlobData[] = { 0xBB, 0xCC, 0xDD, 0xEE, 0xFF };
            const MemoryBlock testBlob (testBlobData, sizeof (testBlobData));
            const uint8 testBlobRepresentation[] = { 0x00, 0x00, 0x00, 0x05, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x00, 0x00 }; // padded to size % 4 == 0

            // write:

            {
                // int32:
                OSCArgument arg (testInt);
                OSCOutputStream outStream;

                expect (outStream.writeArgument (arg));
                expect (outStream.getDataSize() == 4);
                expect (std::memcmp (outStream.getData(), testIntRepresentation, sizeof (testIntRepresentation)) == 0);
            }
            {
                // float32:
                OSCArgument arg (testFloat);
                OSCOutputStream outStream;

                expect (outStream.writeArgument (arg));
                expect (outStream.getDataSize() == 4);
                expect (std::memcmp (outStream.getData(), testFloatRepresentation, sizeof (testFloatRepresentation)) == 0);

            }
            {
                // string:
                expect (testString.length() % 4 != 0); // check whether we actually cover padding
                static_assert (sizeof (testStringRepresentation) % 4 == 0, "Size must be a multiple of 4");

                OSCArgument arg (testString);
                OSCOutputStream outStream;

                expect (outStream.writeArgument (arg));
                expect (outStream.getDataSize() == sizeof (testStringRepresentation));
                expect (std::memcmp (outStream.getData(), testStringRepresentation, sizeof (testStringRepresentation)) == 0);

            }
            {
                // blob:
                expect (testBlob.getSize() % 4 != 0);  // check whether we actually cover padding
                static_assert (sizeof (testBlobRepresentation) % 4 == 0, "Size must be a multiple of 4");

                OSCArgument arg (testBlob);
                OSCOutputStream outStream;

                expect (outStream.writeArgument (arg));
                expect (outStream.getDataSize() == sizeof (testBlobRepresentation));
                expect (std::memcmp (outStream.getData(), testBlobRepresentation, sizeof (testBlobRepresentation)) == 0);

            }
        }

        beginTest ("Writing strings with correct padding");
        {
            // the only OSC-specific thing to check is the correct number of padding zeros

            {
                OSCArgument with15Chars ("123456789012345");
                OSCOutputStream outStream;
                expect (outStream.writeArgument (with15Chars));
                expect (outStream.getDataSize() == 16);
            }
            {
                OSCArgument with16Chars ("1234567890123456");
                OSCOutputStream outStream;
                expect (outStream.writeArgument (with16Chars));
                expect (outStream.getDataSize() == 20);
            }
            {
                OSCArgument with17Chars ("12345678901234567");
                OSCOutputStream outStream;
                expect (outStream.writeArgument (with17Chars));
                expect (outStream.getDataSize() == 20);
            }
            {

                OSCArgument with18Chars ("123456789012345678");
                OSCOutputStream outStream;
                expect (outStream.writeArgument (with18Chars));
                expect (outStream.getDataSize() == 20);
            }
            {

                OSCArgument with19Chars ("1234567890123456789");
                OSCOutputStream outStream;
                expect (outStream.writeArgument (with19Chars));
                expect (outStream.getDataSize() == 20);
            }
            {

                OSCArgument with20Chars ("12345678901234567890");
                OSCOutputStream outStream;
                expect (outStream.writeArgument (with20Chars));
                expect (outStream.getDataSize() == 24);
            }
        }
        beginTest ("Writing blobs with correct padding");
        {
            const char buffer[20] = {};
            {
                OSCArgument with15Bytes (MemoryBlock (buffer, 15));
                OSCOutputStream outStream;
                expect (outStream.writeArgument (with15Bytes));
                expect (outStream.getDataSize() == 20);
            }
            {
                OSCArgument with16Bytes (MemoryBlock (buffer, 16));
                OSCOutputStream outStream;
                expect (outStream.writeArgument (with16Bytes));
                expect (outStream.getDataSize() == 20);
            }
            {
                OSCArgument with17Bytes (MemoryBlock (buffer, 17));
                OSCOutputStream outStream;
                expect (outStream.writeArgument (with17Bytes));
                expect (outStream.getDataSize() == 24);
            }
            {
                OSCArgument with18Bytes (MemoryBlock (buffer, 18));
                OSCOutputStream outStream;
                expect (outStream.writeArgument (with18Bytes));
                expect (outStream.getDataSize() == 24);
            }
            {
                OSCArgument with19Bytes (MemoryBlock (buffer, 19));
                OSCOutputStream outStream;
                expect (outStream.writeArgument (with19Bytes));
                expect (outStream.getDataSize() == 24);
            }
            {
                OSCArgument with20Bytes (MemoryBlock (buffer, 20));
                OSCOutputStream outStream;
                expect (outStream.writeArgument (with20Bytes));
                expect (outStream.getDataSize() == 24);
            }
        }

        beginTest ("Writing OSC messages.");
        {
            {
                int32 testInt = -2015;
                float testFloat = 345.6125f;
                String testString = "Hello, World!";

                const uint8 testBlobData[] = { 0xBB, 0xCC, 0xDD, 0xEE, 0xFF };
                const MemoryBlock testBlob (testBlobData, sizeof (testBlobData));

                uint8 check[52] = { '/', 't', 'e', 's', 't', '\0', '\0', '\0',
                                    ',', 'i', 'f', 's', 'b', '\0', '\0', '\0',
                                    0xFF, 0xFF, 0xF8, 0x21,
                                    0x43, 0xAC, 0xCE, 0x66,
                                    'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\0', '\0', '\0',
                                    0x00, 0x00, 0x00, 0x05, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x00, 0x00
                };

                OSCOutputStream outStream;

                OSCMessage msg ("/test");

                msg.addInt32 (testInt);
                msg.addFloat32 (testFloat);
                msg.addString (testString);
                msg.addBlob (testBlob);

                expect (outStream.writeMessage (msg));
                expect (outStream.getDataSize() == sizeof (check));
                expect (std::memcmp (outStream.getData(), check, sizeof (check)) == 0);
            }
        }

        beginTest ("Writing OSC bundle.");
        {
            {
                int32 testInt = -2015;
                float testFloat = 345.6125f;
                String testString = "Hello, World!";
                const uint8 testBlobData[] = { 0xBB, 0xCC, 0xDD, 0xEE, 0xFF };
                const MemoryBlock testBlob (testBlobData, sizeof (testBlobData));

                uint8 check[] = {
                    '#', 'b', 'u', 'n', 'd', 'l', 'e', '\0',
                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,

                    0x00, 0x00, 0x00, 0x34,

                    '/', 't', 'e', 's', 't', '/', '1', '\0',
                    ',', 'i', 'f', 's', 'b', '\0', '\0', '\0',
                    0xFF, 0xFF, 0xF8, 0x21,
                    0x43, 0xAC, 0xCE, 0x66,
                    'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\0', '\0', '\0',
                    0x00, 0x00, 0x00, 0x05, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x00, 0x00,

                    0x00, 0x00, 0x00, 0x0C,

                    '/', 't', 'e', 's', 't', '/', '2', '\0',
                    ',', '\0', '\0', '\0',

                    0x00, 0x00, 0x00, 0x10,

                    '#', 'b', 'u', 'n', 'd', 'l', 'e', '\0',
                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01
                };

                OSCOutputStream outStream;

                OSCBundle bundle;

                OSCMessage msg1 ("/test/1");
                msg1.addInt32 (testInt);
                msg1.addFloat32 (testFloat);
                msg1.addString (testString);
                msg1.addBlob (testBlob);
                bundle.addElement (msg1);

                OSCMessage msg2 ("/test/2");
                bundle.addElement (msg2);

                OSCBundle subBundle;
                bundle.addElement (subBundle);

                expect (outStream.writeBundle (bundle));
                expect (outStream.getDataSize() == sizeof (check));
                expect (std::memcmp (outStream.getData(), check, sizeof (check)) == 0);
            }
        }
    }
};

static OSCBinaryWriterTests OSCBinaryWriterUnitTests;

//==============================================================================
class OSCRoundTripTests final : public UnitTest
{
public:
    OSCRoundTripTests()
        : UnitTest ("OSCRoundTripTests class", UnitTestCategories::osc)
    {}

    void runTest() override
    {
        beginTest ("Empty OSC message");
        {
            OSCMessage outMessage ("/test/empty");

            OSCOutputStream output;
            output.writeMessage (outMessage);

            OSCInputStream input (output.getData(), output.getDataSize());
            OSCMessage inMessage = input.readMessage();

            expectEquals (inMessage.size(), 0);
        }

        beginTest ("OSC message with single argument");
        {
            OSCMessage outMessage ("/test/one_arg", 42);

            OSCOutputStream output;
            output.writeMessage (outMessage);

            OSCInputStream input (output.getData(), output.getDataSize());
            OSCMessage inMessage = input.readMessage();

            expectEquals (inMessage.size(), 1);
            expectEquals (inMessage[0].getInt32(), 42);
        }

        beginTest ("OSC message with multiple arguments");
        {
            OSCMessage outMessage ("/test/four_args", 42, 0.5f, String ("foo"), String ("bar"));

            OSCOutputStream output;
            output.writeMessage (outMessage);

            OSCInputStream input (output.getData(), output.getDataSize());
            OSCMessage inMessage = input.readMessage();

            expectEquals (inMessage.size(), 4);
            expectEquals (inMessage[0].getInt32(), 42);
            expectEquals (inMessage[1].getFloat32(), 0.5f);
            expectEquals (inMessage[2].getString(), String ("foo"));
            expectEquals (inMessage[3].getString(), String ("bar"));
        }

        beginTest ("Empty OSC bundle");
        {
            OSCBundle outBundle;

            OSCOutputStream output;
            output.writeBundle (outBundle);

            OSCInputStream input (output.getData(), output.getDataSize());
            OSCBundle inBundle = input.readBundle();

            expectEquals (inBundle.size(), 0);
        }

        beginTest ("OSC bundle with single message");
        {
            OSCMessage outMessage ("/test/one_arg", 42);
            OSCBundle outBundle;
            outBundle.addElement (outMessage);

            OSCOutputStream output;
            output.writeBundle (outBundle);

            OSCInputStream input (output.getData(), output.getDataSize());
            OSCBundle inBundle = input.readBundle();

            expectEquals (inBundle.size(), 1);

            OSCMessage inMessage = inBundle[0].getMessage();

            expectEquals (inMessage.getAddressPattern().toString(), String ("/test/one_arg"));
            expectEquals (inMessage.size(), 1);
            expectEquals (inMessage[0].getInt32(), 42);
        }

        beginTest ("OSC bundle with multiple messages");
        {
            OSCMessage outMessage1 ("/test/empty");
            OSCMessage outMessage2 ("/test/one_arg", 42);
            OSCMessage outMessage3 ("/test/four_args", 42, 0.5f, String ("foo"), String ("bar"));

            OSCBundle outBundle;
            outBundle.addElement (outMessage1);
            outBundle.addElement (outMessage2);
            outBundle.addElement (outMessage3);

            OSCOutputStream output;
            output.writeBundle (outBundle);

            OSCInputStream input (output.getData(), output.getDataSize());
            OSCBundle inBundle = input.readBundle();

            expectEquals (inBundle.size(), 3);

            {
                OSCMessage inMessage = inBundle[0].getMessage();

                expectEquals (inMessage.getAddressPattern().toString(), String ("/test/empty"));
                expectEquals (inMessage.size(), 0);
            }
            {
                OSCMessage inMessage = inBundle[1].getMessage();

                expectEquals (inMessage.getAddressPattern().toString(), String ("/test/one_arg"));
                expectEquals (inMessage.size(), 1);
                expectEquals (inMessage[0].getInt32(), 42);
            }
            {
                OSCMessage inMessage = inBundle[2].getMessage();

                expectEquals (inMessage.getAddressPattern().toString(), String ("/test/four_args"));
                expectEquals (inMessage.size(), 4);
                expectEquals (inMessage[0].getInt32(), 42);
                expectEquals (inMessage[1].getFloat32(), 0.5f);
                expectEquals (inMessage[2].getString(), String ("foo"));
                expectEquals (inMessage[3].getString(), String ("bar"));
            }
        }

        beginTest ("OSC bundle containing another bundle");
        {
            OSCBundle outBundleNested;
            outBundleNested.addElement (OSCMessage ("/test/one_arg", 42));

            OSCBundle outBundle;
            outBundle.addElement (outBundleNested);

            OSCOutputStream output;
            output.writeBundle (outBundle);

            OSCInputStream input (output.getData(), output.getDataSize());
            OSCBundle inBundle = input.readBundle();

            expectEquals (inBundle.size(), 1);
            expect (inBundle[0].isBundle());
            OSCBundle inBundleNested = inBundle[0].getBundle();
            expectEquals (inBundleNested.size(), 1);
            expect (inBundleNested[0].isMessage());

            OSCMessage msg = inBundleNested[0].getMessage();

            expectEquals (msg.getAddressPattern().toString(), String ("/test/one_arg"));
            expectEquals (msg.size(), 1);
            expectEquals (msg[0].getInt32(), 42);
        }

        beginTest ("OSC bundle containing multiple other bundles");
        {
            OSCBundle outBundleNested1;
            outBundleNested1.addElement (OSCMessage ("/test/empty"));
            OSCBundle outBundleNested2;
            outBundleNested2.addElement (OSCMessage ("/test/one_arg", 42));

            OSCBundle outBundle;
            outBundle.addElement (outBundleNested1);
            outBundle.addElement (outBundleNested2);

            OSCOutputStream output;
            output.writeBundle (outBundle);

            OSCInputStream input (output.getData(), output.getDataSize());
            OSCBundle inBundle = input.readBundle();

            expectEquals (inBundle.size(), 2);

            {
                expect (inBundle[0].isBundle());
                OSCBundle inBundleNested = inBundle[0].getBundle();
                expectEquals (inBundleNested.size(), 1);
                expect (inBundleNested[0].isMessage());

                OSCMessage msg = inBundleNested[0].getMessage();

                expectEquals (msg.getAddressPattern().toString(), String ("/test/empty"));
                expectEquals (msg.size(), 0);
            }
            {
                expect (inBundle[1].isBundle());
                OSCBundle inBundleNested = inBundle[1].getBundle();
                expectEquals (inBundleNested.size(), 1);
                expect (inBundleNested[0].isMessage());

                OSCMessage msg = inBundleNested[0].getMessage();

                expectEquals (msg.getAddressPattern().toString(), String ("/test/one_arg"));
                expectEquals (msg.size(), 1);
                expectEquals (msg[0].getInt32(), 42);
            }
        }
    }
};

static OSCRoundTripTests OSCRoundTripUnitTests;

#endif

} // namespace juce
