// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.

#include "EditorAnalytics.h"
#include "EditorAnalyticsController.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Vector2.h"
#include "Engine/Core/Types/DateTime.h"
#include "Editor/Editor.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Utilities/StringConverter.h"
#include "FlaxEngine.Gen.h"
#include <ThirdParty/UniversalAnalytics/universal-analytics.h>

#define FLAX_EDITOR_GOOGLE_ID "UA-88357703-3"

// Helper doc: https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters

namespace EditorAnalyticsImpl
{
    UATracker Tracker = nullptr;

    StringAnsi ClientId;
    StringAnsi ProjectName;
    StringAnsi ScreenResolution;
    StringAnsi UserLanguage;
    StringAnsi GPU;
    DateTime SessionStartTime;

    CriticalSection Locker;
    bool IsSessionActive = false;
    EditorAnalyticsController Controller;
    Array<char> TmpBuffer;
}

using namespace EditorAnalyticsImpl;

class EditorAnalyticsService : public EngineService
{
public:

    EditorAnalyticsService()
        : EngineService(TEXT("Editor Analytics"))
    {
    }

    bool Init() override;
    void Dispose() override;
};

EditorAnalyticsService EditorAnalyticsServiceInstance;

bool EditorAnalytics::IsSessionActive()
{
    return EditorAnalyticsImpl::IsSessionActive;
}

void EditorAnalytics::StartSession()
{
    ScopeLock lock(Locker);

    if (EditorAnalyticsImpl::IsSessionActive)
        return;

    // Prepare client metadata
    if (ClientId.IsEmpty())
    {
        ClientId = Platform::GetUniqueDeviceId().ToString(Guid::FormatType::N).ToStringAnsi();
    }
    if (ScreenResolution.IsEmpty())
    {
        const Vector2 desktopSize = Platform::GetDesktopSize();
        ScreenResolution = StringAnsi(StringUtils::ToString((int32)desktopSize.X)) + "x" + StringAnsi(StringUtils::ToString((int32)desktopSize.Y));
    }
    if (UserLanguage.IsEmpty())
    {
        UserLanguage = Platform::GetUserLocaleName().ToStringAnsi();
    }
    if (GPU.IsEmpty())
    {
        const auto gpu = GPUDevice::Instance;
        if (gpu && gpu->GetState() == GPUDevice::DeviceState::Ready)
            GPU = StringAsANSI<>(gpu->GetAdapter()->GetDescription().GetText()).Get();
    }
    if (ProjectName.IsEmpty())
    {
        ProjectName = Editor::Project->Name.ToStringAnsi();
    }
    SessionStartTime = DateTime::Now();

    // Initialize the analytics tracker
    Tracker = createTracker(FLAX_EDITOR_GOOGLE_ID, ClientId.Get(), nullptr);
    Tracker->user_agent = "Flax Editor";

    // Store these options permanently (for the lifetime of the tracker)
    setTrackerOption(Tracker, UA_OPTION_QUEUE, 1);
    UASettings GlobalSettings =
    {
        {
            { UA_DOCUMENT_PATH, 0, "Flax Editor" },
            { UA_DOCUMENT_TITLE, 0, "Flax Editor" },
#if PLATFORM_WINDOWS
            { UA_USER_AGENT, 0, "Windows " FLAXENGINE_VERSION_TEXT },
#else
#error "Unknown platform"
#endif
            { UA_ANONYMIZE_IP, 0, "0" },
            { UA_APP_ID, 0, "Flax Editor " FLAXENGINE_VERSION_TEXT },
            { UA_APP_INSTALLER_ID, 0, "Flax Editor" },
            { UA_APP_NAME, 0, "Flax Editor" },
            { UA_APP_VERSION, 0, FLAXENGINE_VERSION_TEXT },
            { UA_SCREEN_NAME, 0, "Flax Editor " FLAXENGINE_VERSION_TEXT },
            { UA_SCREEN_RESOLUTION, 0, ScreenResolution.Get() },
            { UA_USER_LANGUAGE, 0, UserLanguage.Get() },
        }
    };
    setParameters(Tracker, &GlobalSettings);

    // Send the initial session event
    UAOptions sessionViewOptions =
    {
        {
            { UA_EVENT_CATEGORY, 0, "Session" },
            { UA_EVENT_ACTION, 0, "Start Editor" },
            { UA_EVENT_LABEL, 0, "Start Editor" },
            { UA_SESSION_CONTROL, 0, "start" },
            { UA_DOCUMENT_TITLE, 0, ProjectName.Get() },
        }
    };
    sendTracking(Tracker, UA_SCREENVIEW, &sessionViewOptions);

    EditorAnalyticsImpl::IsSessionActive = true;

    Controller.Init();

    // Report GPU model
    if (GPU.HasChars())
    {
        SendEvent("Telemetry", "GPU.Model", GPU.Get());
    }
}

void EditorAnalytics::EndSession()
{
    ScopeLock lock(Locker);

    if (!EditorAnalyticsImpl::IsSessionActive)
        return;

    Controller.Cleanup();

    StringAnsi sessionLength = StringAnsi::Format("{0}", (int32)(DateTime::Now() - SessionStartTime).GetTotalSeconds());

    // Send the end session event
    UAOptions sessionEventOptions =
    {
        {
            { UA_EVENT_CATEGORY, 0, "Session" },
            { UA_EVENT_ACTION, 0, "Session Length" },
            { UA_EVENT_LABEL, 0, "Session Length" },
            { UA_EVENT_VALUE, 0, sessionLength.Get() },
            { UA_CUSTOM_DIMENSION, 1, "Session Length" },
            { UA_CUSTOM_METRIC, 1, sessionLength.Get() },
        }
    };
    sendTracking(Tracker, UA_EVENT, &sessionEventOptions);

    // Send the end session event
    UAOptions sessionViewOptions =
    {
        {
            { UA_EVENT_CATEGORY, 0, "Session" },
            { UA_EVENT_ACTION, 0, "End Editor" },
            { UA_EVENT_LABEL, 0, "End Editor" },
            { UA_EVENT_VALUE, 0, sessionLength.Get() },
            { UA_SESSION_CONTROL, 0, "end" },
        }
    };
    sendTracking(Tracker, UA_SCREENVIEW, &sessionViewOptions);

    // Cleanup
    removeTracker(Tracker);
    Tracker = nullptr;

    EditorAnalyticsImpl::IsSessionActive = false;
}

void EditorAnalytics::SendEvent(const char* category, const char* name, const char* label)
{
    ScopeLock lock(Locker);

    if (!EditorAnalyticsImpl::IsSessionActive)
        return;

    UAOptions opts =
    {
        {
            { UA_EVENT_CATEGORY, 0, (char*)category },
            { UA_EVENT_ACTION, 0, (char*)name },
            { UA_EVENT_LABEL, 0, (char*)label },
        }
    };

    sendTracking(Tracker, UA_EVENT, &opts);
}

void EditorAnalytics::SendEvent(const char* category, const char* name, const StringView& label)
{
    SendEvent(category, name, label.Get());
}

void EditorAnalytics::SendEvent(const char* category, const char* name, const Char* label)
{
    ScopeLock lock(Locker);

    if (!EditorAnalyticsImpl::IsSessionActive)
        return;

    ASSERT(category && name && label);

    const int32 labelLength = StringUtils::Length(label);
    TmpBuffer.Clear();
    TmpBuffer.Resize(labelLength + 1);
    StringUtils::ConvertUTF162ANSI(label, TmpBuffer.Get(), labelLength);
    TmpBuffer[labelLength] = 0;

    UAOptions opts =
    {
        {
            { UA_EVENT_CATEGORY, 0, (char*)category },
            { UA_EVENT_ACTION, 0, (char*)name },
            { UA_EVENT_LABEL, 0, (char*)TmpBuffer.Get() },
        }
    };

    sendTracking(Tracker, UA_EVENT, &opts);
}

bool EditorAnalyticsService::Init()
{
#if COMPILE_WITH_DEV_ENV
    // Disable analytics from the dev (internal) builds
    LOG(Info, "Editor analytics service is disabled in dev builds.");
    return false;
#endif

    // If file '<editor_install_root>/noTracking' exists then do not use analytics (per engine instance)
    // If file '%appdata/Flax/noTracking' exists then do not use analytics (globally)
    String appDataPath;
    FileSystem::GetSpecialFolderPath(SpecialFolder::AppData, appDataPath);
    if (FileSystem::FileExists(Globals::StartupFolder / TEXT("noTracking")) ||
        FileSystem::FileExists(appDataPath / TEXT("Flax/noTracking")))
    {
        LOG(Info, "Editor analytics service is disabled.");
        return false;
    }

    LOG(Info, "Editor analytics service is enabled. Curl version: {0}", TEXT(LIBCURL_VERSION));

    EditorAnalytics::StartSession();

    return false;
}

void EditorAnalyticsService::Dispose()
{
    EditorAnalytics::EndSession();
}
