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

   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
{

std::optional<Message::Parsed> Parser::parse (Span<const std::byte> message, Status* status)
{
    const auto setStatus = [&] (Status s)
    {
        if (status != nullptr)
            *status = s;
    };

    setStatus (Status::noError);

    Message::Generic generic;

    if (! detail::Marshalling::Reader { message } (generic))
    {
        // Got a full sysex message, but it didn't contain a well-formed header.
        setStatus (Status::malformed);
        return {};
    }

    if ((generic.header.version & std::byte { 0x70 }) != std::byte{})
    {
        setStatus (Status::reservedVersion);
        return Message::Parsed { generic.header, std::monostate{} };
    }

    const auto index = (uint8_t) generic.header.category;
    constexpr auto tables = detail::MessageTypeUtils::getTables();
    const auto processFunction = tables.parsers[index];
    return Message::Parsed { generic.header, processFunction (generic, status) };
}

std::optional<Message::Parsed> Parser::parse (const MUID ourMUID,
                                              Span<const std::byte> message,
                                              Status* status)
{
    const auto setStatus = [&] (Status s)
    {
        if (status != nullptr)
            *status = s;
    };

    setStatus (Status::noError);

    if (const auto parsed = parse (message, status))
    {
        if (parsed->header.destination != MUID::getBroadcast() && parsed->header.destination != ourMUID)
            setStatus (Status::mismatchedMUID);
        else if (parsed->header.source == ourMUID)
            setStatus (Status::collidingMUID);
        else if ((parsed->header.version & std::byte { 0x70 }) != std::byte{})
            setStatus (Status::reservedVersion);

        return parsed;
    }

    return {};
}

class DescriptionVisitor : public detail::MessageTypeUtils::MessageVisitor
{
public:
    DescriptionVisitor (const Message::Parsed* m, String* str) : msg (m), result (str) {}

    void visit (const std::monostate&)                                        const override {}
    void visit (const Message::Discovery& body)                               const override { visitImpl (body); }
    void visit (const Message::DiscoveryResponse& body)                       const override { visitImpl (body); }
    void visit (const Message::InvalidateMUID& body)                          const override { visitImpl (body); }
    void visit (const Message::EndpointInquiry& body)                         const override { visitImpl (body); }
    void visit (const Message::EndpointInquiryResponse& body)                 const override { visitImpl (body); }
    void visit (const Message::ACK& body)                                     const override { visitImpl (body); }
    void visit (const Message::NAK& body)                                     const override { visitImpl (body); }
    void visit (const Message::ProfileInquiry& body)                          const override { visitImpl (body); }
    void visit (const Message::ProfileInquiryResponse& body)                  const override { visitImpl (body); }
    void visit (const Message::ProfileAdded& body)                            const override { visitImpl (body); }
    void visit (const Message::ProfileRemoved& body)                          const override { visitImpl (body); }
    void visit (const Message::ProfileDetails& body)                          const override { visitImpl (body); }
    void visit (const Message::ProfileDetailsResponse& body)                  const override { visitImpl (body); }
    void visit (const Message::ProfileOn& body)                               const override { visitImpl (body); }
    void visit (const Message::ProfileOff& body)                              const override { visitImpl (body); }
    void visit (const Message::ProfileEnabledReport& body)                    const override { visitImpl (body); }
    void visit (const Message::ProfileDisabledReport& body)                   const override { visitImpl (body); }
    void visit (const Message::ProfileSpecificData& body)                     const override { visitImpl (body); }
    void visit (const Message::PropertyExchangeCapabilities& body)            const override { visitImpl (body); }
    void visit (const Message::PropertyExchangeCapabilitiesResponse& body)    const override { visitImpl (body); }
    void visit (const Message::PropertyGetData& body)                         const override { visitImpl (body); }
    void visit (const Message::PropertyGetDataResponse& body)                 const override { visitImpl (body); }
    void visit (const Message::PropertySetData& body)                         const override { visitImpl (body); }
    void visit (const Message::PropertySetDataResponse& body)                 const override { visitImpl (body); }
    void visit (const Message::PropertySubscribe& body)                       const override { visitImpl (body); }
    void visit (const Message::PropertySubscribeResponse& body)               const override { visitImpl (body); }
    void visit (const Message::PropertyNotify& body)                          const override { visitImpl (body); }
    void visit (const Message::ProcessInquiry& body)                          const override { visitImpl (body); }
    void visit (const Message::ProcessInquiryResponse& body)                  const override { visitImpl (body); }
    void visit (const Message::ProcessMidiMessageReport& body)                const override { visitImpl (body); }
    void visit (const Message::ProcessMidiMessageReportResponse& body)        const override { visitImpl (body); }
    void visit (const Message::ProcessEndMidiMessageReport& body)             const override { visitImpl (body); }

private:
    static const char* getDescription (const Message::Discovery&)                            { return "Discovery"; }
    static const char* getDescription (const Message::DiscoveryResponse&)                    { return "Discovery Response"; }
    static const char* getDescription (const Message::InvalidateMUID&)                       { return "Invalidate MUID"; }
    static const char* getDescription (const Message::EndpointInquiry&)                      { return "Endpoint"; }
    static const char* getDescription (const Message::EndpointInquiryResponse&)              { return "Endpoint Response"; }
    static const char* getDescription (const Message::ACK&)                                  { return "ACK"; }
    static const char* getDescription (const Message::NAK&)                                  { return "NAK"; }
    static const char* getDescription (const Message::ProfileInquiry&)                       { return "Profile Inquiry"; }
    static const char* getDescription (const Message::ProfileInquiryResponse&)               { return "Profile Inquiry Response"; }
    static const char* getDescription (const Message::ProfileAdded&)                         { return "Profile Added"; }
    static const char* getDescription (const Message::ProfileRemoved&)                       { return "Profile Removed"; }
    static const char* getDescription (const Message::ProfileDetails&)                       { return "Profile Details"; }
    static const char* getDescription (const Message::ProfileDetailsResponse&)               { return "Profile Details Response"; }
    static const char* getDescription (const Message::ProfileOn&)                            { return "Profile On"; }
    static const char* getDescription (const Message::ProfileOff&)                           { return "Profile Off"; }
    static const char* getDescription (const Message::ProfileEnabledReport&)                 { return "Profile Enabled Report"; }
    static const char* getDescription (const Message::ProfileDisabledReport&)                { return "Profile Disabled Report"; }
    static const char* getDescription (const Message::ProfileSpecificData&)                  { return "Profile Specific Data"; }
    static const char* getDescription (const Message::PropertyExchangeCapabilities&)         { return "Property Exchange Capabilities"; }
    static const char* getDescription (const Message::PropertyExchangeCapabilitiesResponse&) { return "Property Exchange Capabilities Response"; }
    static const char* getDescription (const Message::PropertyGetData&)                      { return "Property Get Data"; }
    static const char* getDescription (const Message::PropertyGetDataResponse&)              { return "Property Get Data Response"; }
    static const char* getDescription (const Message::PropertySetData&)                      { return "Property Set Data"; }
    static const char* getDescription (const Message::PropertySetDataResponse&)              { return "Property Set Data Response"; }
    static const char* getDescription (const Message::PropertySubscribe&)                    { return "Property Subscribe"; }
    static const char* getDescription (const Message::PropertySubscribeResponse&)            { return "Property Subscribe Response"; }
    static const char* getDescription (const Message::PropertyNotify&)                       { return "Property Notify"; }
    static const char* getDescription (const Message::ProcessInquiry&)                       { return "Process Inquiry"; }
    static const char* getDescription (const Message::ProcessInquiryResponse&)               { return "Process Inquiry Response"; }
    static const char* getDescription (const Message::ProcessMidiMessageReport&)             { return "Process Midi Message Report"; }
    static const char* getDescription (const Message::ProcessMidiMessageReportResponse&)     { return "Process Midi Message Report Response"; }
    static const char* getDescription (const Message::ProcessEndMidiMessageReport&)          { return "Process End Midi Message Report"; }

    template <typename Body>
    void visitImpl (const Body& body) const
    {
        const auto opts = ToVarOptions{}.withExplicitVersion ((int) msg->header.version)
                                        .withVersionIncluded (false);
        auto json = ToVar::convert (body, opts);

        if (auto* obj = json->getDynamicObject(); obj != nullptr && obj->hasProperty ("header"))
        {
            const auto header = obj->getProperty ("header");
            const auto bytes = [&]() -> std::vector<std::byte>
            {
                const auto* arr = header.getArray();

                if (arr == nullptr)
                    return {};

                std::vector<std::byte> vec;
                vec.reserve ((size_t) arr->size());

                for (const auto& i : *arr)
                    vec.push_back ((std::byte) (int) i);

                return vec;
            }();

            obj->setProperty ("header", Encodings::jsonFrom7BitText (bytes));
        }

        if (json.has_value())
            *result = String (getDescription (body)) + ": " + JSON::toString (*json, JSON::FormatOptions{}.withSpacing (JSON::Spacing::none));
    }

    const Message::Parsed* msg = nullptr;
    String* result = nullptr;
};

String Parser::getMessageDescription (const Message::Parsed& message)
{
    String result { "!! Unrecognised !!" };
    detail::MessageTypeUtils::visit (message, DescriptionVisitor { &message, &result });
    return result;
}

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

class ParserTests : public UnitTest
{
public:
    ParserTests() : UnitTest ("Parser", UnitTestCategories::midi) {}

    void runTest() override
    {
        auto random = getRandom();

        beginTest ("Sending an empty message does nothing");
        {
            const auto parsed = Parser::parse (MUID::makeRandom (random), {});
            expect (parsed == std::nullopt);
        }

        beginTest ("Sending a garbage message does nothing");
        {
            const std::vector<std::byte> bytes (128, std::byte { 0x70 });
            const auto parsed = Parser::parse (MUID::makeRandom (random), bytes);
            expect (parsed == std::nullopt);
        }

        beginTest ("Sending a message with truncated body produces a malformed status");
        {
            constexpr auto version1 = 0x01;
            const auto truncatedV1 = makeByteArray (0x7e,
                    /* to function block                 */ 0x7f,
                    /* midi CI                           */ 0x0d,
                    /* discovery message                 */ 0x70,
                    /* version                           */ version1,
                    /* source MUID                       */ 0x01,
                    /* ...                               */ 0x02,
                    /* ...                               */ 0x03,
                    /* ...                               */ 0x04,
                    /* broadcast MUID                    */ 0x7f,
                    /* ...                               */ 0x7f,
                    /* ...                               */ 0x7f,
                    /* ...                               */ 0x7f,
                    /* manufacturer                      */ 0x10,
                    /* ...                               */ 0x11,
                    /* ...                               */ 0x12,
                    /* family                            */ 0x20,
                    /* ...                               */ 0x21,
                    /* model                             */ 0x30,
                    /* ...                               */ 0x31,
                    /* revision                          */ 0x40,
                    /* ...                               */ 0x41,
                    /* ...                               */ 0x42,
                    /* ...                               */ 0x43,
                    /* CI category supported             */ 0x7f,
                    /* max sysex size                    */ 0x7f,
                    /* ...                               */ 0x7f,
                    /* ...                               */ 0x7f);
            /* Missing final byte for a version 1 message */
            Parser::Status status{};
            const auto parsedV1 = Parser::parse (MUID::makeRandom (random), truncatedV1, &status);

            expect (status == Parser::Status::malformed);
            expect (parsedV1 == Message::Parsed { Message::Header { ChannelInGroup::wholeBlock,
                                                                    std::byte { 0x70 },
                                                                    std::byte { version1 },
                                                                    MUID::makeUnchecked (0x80c101),
                                                                    MUID::getBroadcast() },
                                                  std::monostate{} });

            constexpr auto version2 = 0x02;
            const auto truncatedV2 = makeByteArray (0x7e,
                    /* to function block                 */ 0x7f,
                    /* midi CI                           */ 0x0d,
                    /* discovery message                 */ 0x70,
                    /* version                           */ version2,
                    /* source MUID                       */ 0x01,
                    /* ...                               */ 0x02,
                    /* ...                               */ 0x03,
                    /* ...                               */ 0x04,
                    /* broadcast MUID                    */ 0x7f,
                    /* ...                               */ 0x7f,
                    /* ...                               */ 0x7f,
                    /* ...                               */ 0x7f,
                    /* manufacturer                      */ 0x10,
                    /* ...                               */ 0x11,
                    /* ...                               */ 0x12,
                    /* family                            */ 0x20,
                    /* ...                               */ 0x21,
                    /* model                             */ 0x30,
                    /* ...                               */ 0x31,
                    /* revision                          */ 0x40,
                    /* ...                               */ 0x41,
                    /* ...                               */ 0x42,
                    /* ...                               */ 0x43,
                    /* CI category supported             */ 0x7f,
                    /* max sysex size                    */ 0x7f,
                    /* ...                               */ 0x7f,
                    /* ...                               */ 0x7f,
                    /* ...                               */ 0x7f);
            /* Missing final byte for a version 2 message */
            const auto parsedV2 = Parser::parse (MUID::makeRandom (random), truncatedV2);

            expect (status == Parser::Status::malformed);
            expect (parsedV2 == Message::Parsed { Message::Header { ChannelInGroup::wholeBlock,
                                                                    std::byte { 0x70 },
                                                                    std::byte { version2 },
                                                                    MUID::makeUnchecked (0x80c101),
                                                                    MUID::getBroadcast() },
                                                  std::monostate{} });
        }

        const auto getExpectedDiscoveryInput = [] (uint8_t version, uint8_t outputPathID)
        {
            return Message::Parsed { Message::Header { ChannelInGroup::wholeBlock,
                                                       std::byte { 0x70 },
                                                       std::byte { version },
                                                       MUID::makeUnchecked (0x80c101),
                                                       MUID::getBroadcast() },
                                     Message::Discovery { { { std::byte { 0x10 }, std::byte { 0x11 }, std::byte { 0x12 } },
                                                            { std::byte { 0x20 }, std::byte { 0x21 } },
                                                            { std::byte { 0x30 }, std::byte { 0x31 } },
                                                            { std::byte { 0x40 }, std::byte { 0x41 }, std::byte { 0x42 }, std::byte { 0x43 } } },
                                                          std::byte { 0x7f },
                                                          0xfffffff,
                                                          std::byte { outputPathID } } };
        };

        beginTest ("Sending a V1 discovery message notifies the input listener");
        {
            const auto initialMUID = MUID::makeRandom (random);
            constexpr uint8_t version = 0x01;

            const auto bytes = makeByteArray (0x7e,
                    /* to function block           */ 0x7f,
                    /* midi CI                     */ 0x0d,
                    /* discovery message           */ 0x70,
                    /* version                     */ version,
                    /* source MUID                 */ 0x01,
                    /* ...                         */ 0x02,
                    /* ...                         */ 0x03,
                    /* ...                         */ 0x04,
                    /* broadcast MUID              */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* manufacturer                */ 0x10,
                    /* ...                         */ 0x11,
                    /* ...                         */ 0x12,
                    /* family                      */ 0x20,
                    /* ...                         */ 0x21,
                    /* model                       */ 0x30,
                    /* ...                         */ 0x31,
                    /* revision                    */ 0x40,
                    /* ...                         */ 0x41,
                    /* ...                         */ 0x42,
                    /* ...                         */ 0x43,
                    /* CI category supported       */ 0x7f,
                    /* max sysex size              */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* ...                         */ 0x7f);
            const auto parsed = Parser::parse (initialMUID, bytes);

            expect (parsed == getExpectedDiscoveryInput (version, 0));
        }

        beginTest ("Sending a V2 discovery message notifies the input listener");
        {
            constexpr uint8_t outputPathID = 5;
            const auto initialMUID = MUID::makeRandom (random);
            constexpr uint8_t version = 0x02;

            const auto bytes = makeByteArray (0x7e,
                    /* to function block           */ 0x7f,
                    /* midi CI                     */ 0x0d,
                    /* discovery message           */ 0x70,
                    /* version                     */ version,
                    /* source MUID                 */ 0x01,
                    /* ...                         */ 0x02,
                    /* ...                         */ 0x03,
                    /* ...                         */ 0x04,
                    /* broadcast MUID              */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* manufacturer                */ 0x10,
                    /* ...                         */ 0x11,
                    /* ...                         */ 0x12,
                    /* family                      */ 0x20,
                    /* ...                         */ 0x21,
                    /* model                       */ 0x30,
                    /* ...                         */ 0x31,
                    /* revision                    */ 0x40,
                    /* ...                         */ 0x41,
                    /* ...                         */ 0x42,
                    /* ...                         */ 0x43,
                    /* CI category supported       */ 0x7f,
                    /* max sysex size              */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* output path ID              */ outputPathID);
            const auto parsed = Parser::parse (initialMUID, bytes);

            expect (parsed == getExpectedDiscoveryInput (version, outputPathID));
        }

        beginTest ("Sending a discovery message with a future version notifies the input listener and ignores trailing fields");
        {
            constexpr uint8_t outputPathID = 10;
            const auto initialMUID = MUID::makeRandom (random);
            constexpr auto version = (uint8_t) detail::MessageMeta::implementationVersion + 1;

            const auto bytes = makeByteArray (0x7e,
                    /* to function block           */ 0x7f,
                    /* midi CI                     */ 0x0d,
                    /* discovery message           */ 0x70,
                    /* version                     */ version,
                    /* source MUID                 */ 0x01,
                    /* ...                         */ 0x02,
                    /* ...                         */ 0x03,
                    /* ...                         */ 0x04,
                    /* broadcast MUID              */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* manufacturer                */ 0x10,
                    /* ...                         */ 0x11,
                    /* ...                         */ 0x12,
                    /* family                      */ 0x20,
                    /* ...                         */ 0x21,
                    /* model                       */ 0x30,
                    /* ...                         */ 0x31,
                    /* revision                    */ 0x40,
                    /* ...                         */ 0x41,
                    /* ...                         */ 0x42,
                    /* ...                         */ 0x43,
                    /* CI category supported       */ 0x7f,
                    /* max sysex size              */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* ...                         */ 0x7f,
                    /* output path ID              */ outputPathID,
                    /* extra bytes                 */ 0x00,
                    /* ...                         */ 0x00,
                    /* ...                         */ 0x00,
                    /* ...                         */ 0x00);
            const auto parsed = Parser::parse (initialMUID, bytes);

            expect (parsed == getExpectedDiscoveryInput (version, outputPathID));
        }
    }

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)... };
    }
};

static ParserTests parserTests;

#endif

} // namespace juce::midi_ci
