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

   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.

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


namespace juce::midi_ci::detail
{

Parser::Status Responder::processCompleteMessage (BufferOutput& output,
                                                  ump::BytesOnGroup message,
                                                  Span<ResponderDelegate* const> listeners)
{
    auto status = Parser::Status::noError;
    const auto parsed = Parser::parse (output.getMuid(), message.bytes, &status);

    if (! parsed.has_value())
        return Parser::Status::malformed;

    class Output : public ResponderOutput
    {
    public:
        Output (BufferOutput& o, Message::Header h, uint8_t g)
            : innerOutput (o), header (h), group (g) {}

        MUID getMuid() const override { return innerOutput.getMuid(); }
        Message::Header getIncomingHeader() const override { return header; }
        uint8_t getIncomingGroup() const override { return group; }
        std::vector<std::byte>& getOutputBuffer() override { return innerOutput.getOutputBuffer(); }
        void send (uint8_t g) override { innerOutput.send (g); }

    private:
        BufferOutput& innerOutput;
        Message::Header header;
        uint8_t group{};
    };

    Output responderOutput { output, parsed->header, message.group };

    if (status != Parser::Status::noError)
    {
        switch (status)
        {
            case Parser::Status::collidingMUID:
            {
                const Message::Header header { ChannelInGroup::wholeBlock,
                                               MessageMeta::Meta<Message::InvalidateMUID>::subID2,
                                               MessageMeta::implementationVersion,
                                               output.getMuid(),
                                               MUID::getBroadcast() };
                const Message::InvalidateMUID body { output.getMuid() };
                MessageTypeUtils::send (responderOutput, responderOutput.getIncomingGroup(), header, body);
                break;
            }

            case Parser::Status::unrecognisedMessage:
                MessageTypeUtils::sendNAK (responderOutput, std::byte { 0x01 });
                break;

            case Parser::Status::reservedVersion:
                MessageTypeUtils::sendNAK (responderOutput, std::byte { 0x02 });
                break;

            case Parser::Status::malformed:
                MessageTypeUtils::sendNAK (responderOutput, std::byte { 0x41 });
                break;

            case Parser::Status::mismatchedMUID:
            case Parser::Status::noError:
                break;
        }

        return status;
    }

    for (auto* listener : listeners)
        if (listener != nullptr && listener->tryRespond (responderOutput, *parsed))
            return Parser::Status::noError;

    MessageTypeUtils::BaseCaseDelegate base;

    if (base.tryRespond (responderOutput, *parsed))
        return Parser::Status::noError;

    return Parser::Status::unrecognisedMessage;
}

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

class ResponderTests : public UnitTest
{
public:
    ResponderTests() : UnitTest ("Responder", UnitTestCategories::midi) {}

    void runTest() override
    {
        auto random = getRandom();
        std::vector<std::byte> outgoing;

        const auto makeOutput = [&]
        {
            struct Output : public BufferOutput
            {
                Output (Random& r, std::vector<std::byte>& b)
                    : muid (MUID::makeRandom (r)), buf (b) {}

                MUID getMuid() const override { return muid; }
                std::vector<std::byte>& getOutputBuffer() override { return buf; }
                void send (uint8_t) override { sent.push_back (buf); }

                MUID muid;
                std::vector<std::byte>& buf;
                std::vector<std::vector<std::byte>> sent;
            };

            return Output { random, outgoing };
        };

        beginTest ("An endpoint message with a matching MUID provokes an endpoint response");
        {
            constexpr auto version = MessageMeta::implementationVersion;

            auto output = makeOutput();
            const auto initialMUID = output.getMuid();

            const auto bytes = makeByteArray (0x7e,
            /* to function block           */ 0x7f,
            /* midi CI                     */ 0x0d,
            /* endpoint message            */ 0x72,
            /* version                     */ version,
            /* source MUID                 */ 0x01,
            /* ...                         */ 0x02,
            /* ...                         */ 0x03,
            /* ...                         */ 0x04,
            /* destination MUID            */ (initialMUID.get() >> 0x00) & 0x7f,
            /* ...                         */ (initialMUID.get() >> 0x07) & 0x7f,
            /* ...                         */ (initialMUID.get() >> 0x0e) & 0x7f,
            /* ...                         */ (initialMUID.get() >> 0x15) & 0x7f,
            /* status, product instance ID */ 0x00);

            const Message::Parsed expectedInput { Message::Header { ChannelInGroup::wholeBlock,
                                                                    std::byte { 0x72 },
                                                                    version,
                                                                    MUID::makeUnchecked (0x80c101),
                                                                    initialMUID },
                                                  Message::EndpointInquiry { std::byte { 0x00 } } };
            EndpointResponderListener listener;
            processCompleteMessage (output, { 0, bytes }, listener);
            expect (listener == SilentResponderListener (expectedInput));

            const auto expectedOutputBytes = makeByteArray (0x7e,
            /* to function block                         */ 0x7f,
            /* midi CI                                   */ 0x0d,
            /* endpoint reply                            */ 0x73,
            /* version                                   */ version,
            /* source MUID                               */ (initialMUID.get() >> 0x00) & 0x7f,
            /* ...                                       */ (initialMUID.get() >> 0x07) & 0x7f,
            /* ...                                       */ (initialMUID.get() >> 0x0e) & 0x7f,
            /* ...                                       */ (initialMUID.get() >> 0x15) & 0x7f,
            /* destination MUID                          */ 0x01,
            /* ...                                       */ 0x02,
            /* ...                                       */ 0x03,
            /* ...                                       */ 0x04,
            /* status                                    */ 0x00,
            /* 16-bit length of following data           */ 0x04,
            /* ...                                       */ 0x00,
            /* info                                      */ 0x00,
            /* ...                                       */ 0x00,
            /* ...                                       */ 0x00,
            /* ...                                       */ 0x00);

            expect (rangesEqual (output.sent.front(), expectedOutputBytes));
        }

        beginTest ("An endpoint message directed at a different MUID does not provoke a response");
        {
            const auto destMUID = MUID::makeRandom (random);
            constexpr auto version = MessageMeta::implementationVersion;

            const auto bytes = makeByteArray (0x7e,
            /* to function block           */ 0x7f,
            /* midi CI                     */ 0x0d,
            /* endpoint message            */ 0x72,
            /* version                     */ version,
            /* source MUID                 */ 0x01,
            /* ...                         */ 0x02,
            /* ...                         */ 0x03,
            /* ...                         */ 0x04,
            /* destination MUID            */ (destMUID.get() >> 0x00) & 0x7f,
            /* ...                         */ (destMUID.get() >> 0x07) & 0x7f,
            /* ...                         */ (destMUID.get() >> 0x0e) & 0x7f,
            /* ...                         */ (destMUID.get() >> 0x15) & 0x7f,
            /* status, product instance ID */ 0x00);

            auto output = makeOutput();
            EndpointResponderListener listener;
            processCompleteMessage (output, { 0, bytes }, listener);
            expect (listener == SilentResponderListener());
            expect (output.sent.empty());
        }

        beginTest ("If the listener fails to compose an endpoint response, a NAK is emitted");
        {
            auto output = makeOutput();
            const auto initialMUID = output.getMuid();

            SilentResponderListener listener;
            constexpr auto version = MessageMeta::implementationVersion;

            const auto bytes = makeByteArray (0x7e,
            /* to function block           */ 0x7f,
            /* midi CI                     */ 0x0d,
            /* endpoint message            */ 0x72,
            /* version                     */ version,
            /* source MUID                 */ 0x01,
            /* ...                         */ 0x02,
            /* ...                         */ 0x03,
            /* ...                         */ 0x04,
            /* destination MUID            */ (initialMUID.get() >> 0x00) & 0x7f,
            /* ...                         */ (initialMUID.get() >> 0x07) & 0x7f,
            /* ...                         */ (initialMUID.get() >> 0x0e) & 0x7f,
            /* ...                         */ (initialMUID.get() >> 0x15) & 0x7f,
            /* status, product instance ID */ 0x00);

            processCompleteMessage (output, { 0, bytes }, listener);

            const auto expectedOutputBytes = makeByteArray (0x7e,
            /* to function block                         */ 0x7f,
            /* midi CI                                   */ 0x0d,
            /* nak                                       */ 0x7f,
            /* version                                   */ version,
            /* source MUID                               */ (initialMUID.get() >> 0x00) & 0x7f,
            /* ...                                       */ (initialMUID.get() >> 0x07) & 0x7f,
            /* ...                                       */ (initialMUID.get() >> 0x0e) & 0x7f,
            /* ...                                       */ (initialMUID.get() >> 0x15) & 0x7f,
            /* destination MUID                          */ 0x01,
            /* ...                                       */ 0x02,
            /* ...                                       */ 0x03,
            /* ...                                       */ 0x04,
            /* original transaction sub-id #2            */ 0x72,
            /* nak status code                           */ 0x00,
            /* nak status data                           */ 0x00,
            /* details                                   */ 0x00,
            /* ...                                       */ 0x00,
            /* ...                                       */ 0x00,
            /* ...                                       */ 0x00,
            /* ...                                       */ 0x00,
            /* message text length                       */ 0x00,
            /* ...                                       */ 0x00);
            expect (rangesEqual (output.sent.front(), expectedOutputBytes));
        }

        beginTest ("If a message is sent with reserved bits set in the Message Format Version, a NAK is emitted");
        {
            auto output = makeOutput();
            const auto initialMUID = output.getMuid();

            const auto bytes = makeByteArray (0x7e,
            /* to function block           */ 0x7f,
            /* midi CI                     */ 0x0d,
            /* endpoint message            */ 0x72,
            /* version,  reserved bit set  */ 0x12,
            /* source MUID                 */ 0x01,
            /* ...                         */ 0x02,
            /* ...                         */ 0x03,
            /* ...                         */ 0x04,
            /* destination MUID            */ (initialMUID.get() >> 0x00) & 0x7f,
            /* ...                         */ (initialMUID.get() >> 0x07) & 0x7f,
            /* ...                         */ (initialMUID.get() >> 0x0e) & 0x7f,
            /* ...                         */ (initialMUID.get() >> 0x15) & 0x7f,
            /* status, product instance ID */ 0x00);
            SilentResponderListener listener;
            processCompleteMessage (output, { 0, bytes }, listener);

            expect (listener == SilentResponderListener{});

            const auto expectedOutputBytes = makeByteArray (0x7e,
            /* to function block                         */ 0x7f,
            /* midi CI                                   */ 0x0d,
            /* nak                                       */ 0x7f,
            /* version                                   */ MessageMeta::implementationVersion,
            /* source MUID                               */ (initialMUID.get() >> 0x00) & 0x7f,
            /* ...                                       */ (initialMUID.get() >> 0x07) & 0x7f,
            /* ...                                       */ (initialMUID.get() >> 0x0e) & 0x7f,
            /* ...                                       */ (initialMUID.get() >> 0x15) & 0x7f,
            /* destination MUID                          */ 0x01,
            /* ...                                       */ 0x02,
            /* ...                                       */ 0x03,
            /* ...                                       */ 0x04,
            /* original transaction sub-id #2            */ 0x72,
            /* nak status code                           */ 0x02,
            /* nak status data                           */ 0x00,
            /* details                                   */ 0x00,
            /* ...                                       */ 0x00,
            /* ...                                       */ 0x00,
            /* ...                                       */ 0x00,
            /* ...                                       */ 0x00,
            /* message text length                       */ 0x00,
            /* ...                                       */ 0x00);

            expect (rangesEqual (output.sent.front(), expectedOutputBytes));
        }

        beginTest ("If the message body is malformed, a NAK with a status of 0x41 is emitted");
        {
            const auto sourceMUID = MUID::makeRandom (random);

            Message::Header header;
            header.deviceID = ChannelInGroup::wholeBlock;
            header.category = std::byte { 0x7e };
            header.version = MessageMeta::implementationVersion;
            header.source = sourceMUID;
            header.destination = MUID::getBroadcast();

            Message::InvalidateMUID invalidate;
            invalidate.target = MUID::makeRandom (random);

            std::vector<std::byte> message;
            Marshalling::Writer { message } (header, invalidate);

            // Remove a byte from the end of the message
            message.pop_back();

            auto output = makeOutput();
            const auto ourMUID = output.getMuid();

            SilentResponderListener listener;
            processCompleteMessage (output, { 0, message }, listener);

            const auto expectedOutputBytes = makeByteArray (0x7e,
            /* to function block                         */ 0x7f,
            /* midi CI                                   */ 0x0d,
            /* nak                                       */ 0x7f,
            /* version                                   */ MessageMeta::implementationVersion,
            /* source MUID                               */ (ourMUID.get() >> 0x00) & 0x7f,
            /* ...                                       */ (ourMUID.get() >> 0x07) & 0x7f,
            /* ...                                       */ (ourMUID.get() >> 0x0e) & 0x7f,
            /* ...                                       */ (ourMUID.get() >> 0x15) & 0x7f,
            /* destination MUID                          */ (sourceMUID.get() >> 0x00) & 0x7f,
            /* ...                                       */ (sourceMUID.get() >> 0x07) & 0x7f,
            /* ...                                       */ (sourceMUID.get() >> 0x0e) & 0x7f,
            /* ...                                       */ (sourceMUID.get() >> 0x15) & 0x7f,
            /* original transaction sub-id #2            */ 0x7e,
            /* nak status code                           */ 0x41,
            /* nak status data                           */ 0x00,
            /* details                                   */ 0x00,
            /* ...                                       */ 0x00,
            /* ...                                       */ 0x00,
            /* ...                                       */ 0x00,
            /* ...                                       */ 0x00,
            /* message text length                       */ 0x00,
            /* ...                                       */ 0x00);
            expect (rangesEqual (output.sent.front(), expectedOutputBytes));
        }

        beginTest ("If an unrecognised message is received, a NAK with a status of 0x01 is emitted");
        {
            const auto sourceMUID = MUID::makeRandom (random);

            Message::Header header;
            header.deviceID = ChannelInGroup::wholeBlock;
            header.category = std::byte { 0x50 }; // reserved
            header.version = MessageMeta::implementationVersion;
            header.source = sourceMUID;
            header.destination = MUID::getBroadcast();

            std::vector<std::byte> message;
            Marshalling::Writer { message } (header);
            message.emplace_back();

            auto output = makeOutput();
            const auto ourMUID = output.getMuid();

            SilentResponderListener listener;
            processCompleteMessage (output, { 0, message }, listener);

            const auto expectedOutputBytes = makeByteArray (0x7e,
            /* to function block                         */ 0x7f,
            /* midi CI                                   */ 0x0d,
            /* nak                                       */ 0x7f,
            /* version                                   */ MessageMeta::implementationVersion,
            /* source MUID                               */ (ourMUID.get() >> 0x00) & 0x7f,
            /* ...                                       */ (ourMUID.get() >> 0x07) & 0x7f,
            /* ...                                       */ (ourMUID.get() >> 0x0e) & 0x7f,
            /* ...                                       */ (ourMUID.get() >> 0x15) & 0x7f,
            /* destination MUID                          */ (sourceMUID.get() >> 0x00) & 0x7f,
            /* ...                                       */ (sourceMUID.get() >> 0x07) & 0x7f,
            /* ...                                       */ (sourceMUID.get() >> 0x0e) & 0x7f,
            /* ...                                       */ (sourceMUID.get() >> 0x15) & 0x7f,
            /* original transaction sub-id #2            */ 0x50,
            /* nak status code                           */ 0x01,
            /* nak status data                           */ 0x00,
            /* details                                   */ 0x00,
            /* ...                                       */ 0x00,
            /* ...                                       */ 0x00,
            /* ...                                       */ 0x00,
            /* ...                                       */ 0x00,
            /* message text length                       */ 0x00,
            /* ...                                       */ 0x00);
            expect (rangesEqual (output.sent.front(), expectedOutputBytes));
        }
    }

private:
    template <typename... Ts>
    static std::array<std::byte, sizeof... (Ts)> makeByteArray (Ts&&... ts)
    {
        jassert (((0 <= (int) ts && (int) ts <= std::numeric_limits<uint8_t>::max()) && ...));
        return { std::byte (ts)... };
    }

    struct SilentResponderListener : public ResponderDelegate
    {
        SilentResponderListener() = default;
        explicit SilentResponderListener (const Message::Parsed& p) : parsed (p) {}

        bool tryRespond (ResponderOutput&, const Message::Parsed& p) override
        {
            parsed = p;
            return false;
        }

        // Returning false indicates that the message was not handled
        bool operator== (const SilentResponderListener& other) const { return parsed == other.parsed; }
        bool operator!= (const SilentResponderListener& other) const { return ! operator== (other); }

        std::optional<Message::Parsed> parsed;
    };

    struct EndpointResponderListener : public SilentResponderListener
    {
        bool tryRespond (ResponderOutput& output, const Message::Parsed& message) override
        {
            parsed = message;

            if (std::holds_alternative<Message::EndpointInquiry> (message.body))
            {
                std::array<std::byte, 4> data{};
                Message::EndpointInquiryResponse response;
                response.status = std::byte{};
                response.data = data;

                MessageTypeUtils::send (output, output.getIncomingGroup(), output.getReplyHeader (std::byte { 0x73 }), response);
                return true;
            }

            return SilentResponderListener::tryRespond (output, message);

        }

        using SilentResponderListener::operator==, SilentResponderListener::operator!=;
    };

    struct OutputCallback
    {
        void operator() (Span<const std::byte> bytes)
        {
            output = std::vector<std::byte> (bytes.begin(), bytes.end());
        }

        std::vector<std::byte> output;
    };

    template <typename A, typename B>
    static bool rangesEqual (A&& a, B&& b)
    {
        using std::begin, std::end;
        return std::equal (begin (a), end (a), begin (b), end (b));
    }


    static Parser::Status processCompleteMessage (BufferOutput& output,
                                                  ump::BytesOnGroup message,
                                                  ResponderDelegate& listener)
    {
        ResponderDelegate* const listeners[] { &listener };
        return Responder::processCompleteMessage (output, message, listeners);
    }
};

static ResponderTests responderTests;

#endif

} // namespace juce::midi_ci::detail
