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

   This file is part of the JUCE library.
   Copyright (c) 2017 - ROLI Ltd.

   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 5 End-User License
   Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
   27th April 2017).

   End User License Agreement: www.juce.com/juce-5-licence
   Privacy Policy: www.juce.com/juce-5-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.

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

#include "jucer_ProjucerAnalytics.h"

//==============================================================================
ProjucerAnalyticsDestination::ProjucerAnalyticsDestination()
    : ThreadedAnalyticsDestination ("ProjucerAnalyticsThread")
{
    {
        MemoryOutputStream mo;
        if (Base64::convertFromBase64 (mo, BinaryData::nothingtoseehere_txt))
            apiKey = mo.toString();
    }

    auto dataDir = File::getSpecialLocation (File::userApplicationDataDirectory)
                        #if JUCE_MAC
                        .getChildFile ("Application Support")
                        #endif
                        .getChildFile ("Projucer")
                        .getChildFile ("Analytics");

    if (! dataDir.exists())
        dataDir.createDirectory();

    savedEventsFile = dataDir.getChildFile ("analytics_events.xml");

    startAnalyticsThread (initialPeriodMs);
}

ProjucerAnalyticsDestination::~ProjucerAnalyticsDestination()
{
    Thread::sleep (initialPeriodMs);

    stopAnalyticsThread (1000);
}

//==============================================================================
static void setData (const AnalyticsDestination::AnalyticsEvent& event, StringPairArray& data)
{
    data.set ("ea",  event.name);

    if (event.parameters.getAllKeys().contains ("label"))
        data.set ("el", event.parameters.getValue ("label", {}));

    data.addArray (event.userProperties);
}

bool ProjucerAnalyticsDestination::logBatchedEvents (const Array<AnalyticsEvent>& events)
{
    String appData ("v=1&aip=1&tid=" + apiKey);

    StringArray postData;

    for (auto& event : events)
    {
        StringPairArray data;

        data.set ("t", "event");
        data.set ("cid", event.userID);

        switch (event.eventType)
        {
            case ProjucerAnalyticsEvent::appEvent:
            {
                data.set ("ec", "App");
                setData (event, data);

                break;
            }

            case ProjucerAnalyticsEvent::projectEvent:
            {
                data.set ("ec", "Project");
                setData (event, data);

                break;
            }

            case ProjucerAnalyticsEvent::userEvent:
            {
                data.set ("ec", "User");
                setData (event, data);

                break;
            }

            case ProjucerAnalyticsEvent::exampleEvent:
            {
                data.set ("ec", "Example");
                setData (event, data);

                break;
            }

            case ProjucerAnalyticsEvent::startPageEvent:
            {
                data.set ("ec", "Start Page");
                setData (event, data);

                break;
            }

            default:
            {
                // unknown event type!
                jassertfalse;
                break;
            }
        }

        StringArray eventData;

        for (auto& key : data.getAllKeys())
            eventData.add (key + "=" + URL::addEscapeChars (data[key], true));

        postData.add (appData + "&" + eventData.joinIntoString ("&"));
    }

    auto url = URL ("https://www.google-analytics.com/batch")
                   .withPOSTData (postData.joinIntoString ("\n"));

    {
        const ScopedLock lock (webStreamCreation);

        if (shouldExit)
            return false;

        webStream.reset (new WebInputStream (url, true));
    }

    auto success = webStream->connect (nullptr);

    // Do an exponential backoff if we failed to connect.
    if (success)
        periodMs = initialPeriodMs;
    else
        periodMs *= 2;

    setBatchPeriod (periodMs);

    return success;
}

void ProjucerAnalyticsDestination::stopLoggingEvents()
{
    const ScopedLock lock (webStreamCreation);

    shouldExit = true;

    if (webStream.get() != nullptr)
        webStream->cancel();
}

//==============================================================================
void ProjucerAnalyticsDestination::saveUnloggedEvents (const std::deque<AnalyticsEvent>& eventsToSave)
{
    XmlDocument previouslySavedEvents (savedEventsFile);
    std::unique_ptr<XmlElement> xml (previouslySavedEvents.getDocumentElement());

    if (xml.get() == nullptr || xml->getTagName() != "events")
        xml.reset (new XmlElement ("events"));

    for (auto& event : eventsToSave)
    {
        auto* xmlEvent = new XmlElement ("google_analytics_event");
        xmlEvent->setAttribute ("name", event.name);
        xmlEvent->setAttribute ("type", event.eventType);
        xmlEvent->setAttribute ("timestamp", (int) event.timestamp);
        xmlEvent->setAttribute ("user_id", event.userID);

        auto* parameters = new XmlElement ("parameters");

        for (auto& key : event.parameters.getAllKeys())
            parameters->setAttribute (key, event.parameters[key]);

        xmlEvent->addChildElement (parameters);

        auto* userProperties = new XmlElement ("user_properties");

        for (auto& key : event.userProperties.getAllKeys())
            userProperties->setAttribute (key, event.userProperties[key]);

        xmlEvent->addChildElement (userProperties);

        xml->addChildElement (xmlEvent);
    }

    xml->writeToFile (savedEventsFile, {});
}

void ProjucerAnalyticsDestination::restoreUnloggedEvents (std::deque<AnalyticsEvent>& restoredEventQueue)
{
    XmlDocument savedEvents (savedEventsFile);
    std::unique_ptr<XmlElement> xml (savedEvents.getDocumentElement());

    if (xml.get() == nullptr || xml->getTagName() != "events")
        return;

    auto numEvents = xml->getNumChildElements();

    for (int iEvent = 0; iEvent < numEvents; ++iEvent)
    {
        auto* xmlEvent = xml->getChildElement (iEvent);

        StringPairArray parameters;
        auto* xmlParameters = xmlEvent->getChildByName ("parameters");
        auto numParameters = xmlParameters->getNumAttributes();

        for (int iParam = 0; iParam < numParameters; ++iParam)
            parameters.set (xmlParameters->getAttributeName (iParam),
                            xmlParameters->getAttributeValue (iParam));

        StringPairArray userProperties;
        auto* xmlUserProperties = xmlEvent->getChildByName ("user_properties");
        auto numUserProperties = xmlUserProperties->getNumAttributes();

        for (int iProp = 0; iProp < numUserProperties; ++iProp)
            userProperties.set (xmlUserProperties->getAttributeName (iProp),
                                xmlUserProperties->getAttributeValue (iProp));

        restoredEventQueue.push_back ({
            xmlEvent->getStringAttribute ("name"),
            xmlEvent->getIntAttribute ("type"),
            static_cast<uint32> (xmlEvent->getIntAttribute ("timestamp")),
            parameters,
            xmlEvent->getStringAttribute ("user_id"),
            userProperties
        });
    }

    savedEventsFile.deleteFile();
}
