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

   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.

  ==============================================================================
*/
#pragma once

//==============================================================================
struct NetWorkerThread   : public Thread,
                           private AsyncUpdater
{
    NetWorkerThread()  : Thread ("License") {}

    ~NetWorkerThread()
    {
        jassert (MessageManager::getInstance()->isThisTheMessageThread());

        signalThreadShouldExit();
        cancelPendingUpdate();
        finished.signal();

        {
            ScopedLock lock (weakReferenceLock);

            if (currentInputStream != nullptr)
                currentInputStream->cancel();
        }

        waitForThreadToExit (-1);
    }

    //==============================================================================
    void executeOnMessageThreadAndBlock (std::function<void()> f, bool signalWhenFinished = true)
    {
        // only call this on the worker thread
        jassert (Thread::getCurrentThreadId() == getThreadId());

        if (! isWaiting)
        {
            ScopedValueSetter<bool> reentrant (isWaiting, true);

            finished.reset();

            if (! threadShouldExit())
            {
                functionToExecute = [signalWhenFinished, f, this] () { f(); if (signalWhenFinished) finished.signal(); };
                triggerAsyncUpdate();
                finished.wait (-1);
            }
        }
        else
        {
            // only one task at a time
            jassertfalse;
            return;
        }
    }

    WebInputStream* getSharedWebInputStream (const URL& url, const bool usePost)
    {
        ScopedLock lock (weakReferenceLock);

        if (threadShouldExit())
            return nullptr;

        jassert (currentInputStream == nullptr);
        return (currentInputStream = new WeakWebInputStream (*this, url, usePost));
    }

    bool isWaiting = false;
    WaitableEvent finished;

private:
    //==============================================================================
    void handleAsyncUpdate() override
    {
        if (functionToExecute)
        {
            std::function<void()> f;
            std::swap (f, functionToExecute);

            if (! threadShouldExit())
                f();
        }
    }

    //==============================================================================
    struct WeakWebInputStream  : public WebInputStream
    {
        WeakWebInputStream (NetWorkerThread& workerThread, const URL& url, const bool usePost)
            : WebInputStream (url, usePost), owner (workerThread) {}

        ~WeakWebInputStream()
        {
            ScopedLock lock (owner.weakReferenceLock);
            owner.currentInputStream = nullptr;
        }

        NetWorkerThread& owner;
        WeakReference<WeakWebInputStream>::Master masterReference;
        friend class WeakReference<WeakWebInputStream>;
    };

    //==============================================================================
    friend struct WeakWebInputStream;

    std::function<void()> functionToExecute;
    CriticalSection weakReferenceLock;
    WebInputStream* currentInputStream = nullptr;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NetWorkerThread)
};

//==============================================================================
//==============================================================================
//==============================================================================
struct LicenseThread : NetWorkerThread
{
    LicenseThread (LicenseController& licenseController, bool shouldSelectNewLicense)
        : owner (licenseController), selectNewLicense (shouldSelectNewLicense)
    {
        startThread();
    }

    String getAuthToken()
    {
        if (owner.state.authToken.isNotEmpty())
            return owner.state.authToken;

        selectNewLicense = false;
        HashMap<String, String> result;

        if (! queryWebview ("https://auth.roli.com/signin/projucer?redirect=projucer://receive-auth-token?token=",
                            "receive-auth-token", result))
            return {};

        return result["token"];
    }

    // returns true if any information was updated
    void updateUserInfo (LicenseState& stateToUpdate)
    {
        jassert (stateToUpdate.authToken.isNotEmpty());

        auto accessTokenHeader = "x-access-token: " + stateToUpdate.authToken;

        if (ScopedPointer<WebInputStream> shared
              = getSharedWebInputStream (URL ("https://api.roli.com/api/v1/user"), false))
        {
            const int statusCode = shared->withExtraHeaders (accessTokenHeader).getStatusCode();

            if (statusCode == 200)
            {
                var result = JSON::parse (shared->readEntireStreamAsString());
                shared = nullptr;

                auto newState = licenseStateFromJSON (result, stateToUpdate.authToken, stateToUpdate.avatar);

                if (newState.type != LicenseState::Type::notLoggedIn)
                    stateToUpdate = newState;
            }
            else if (statusCode == 401)
            {
                selectNewLicense = false;

                // un-authorised: token has expired
                stateToUpdate = LicenseState();
            }
        }
    }

    void updateLicenseType (LicenseState& stateToUpdate)
    {
        bool requiredWebview = false;
        String licenseChooserPage = "https://juce.com/webviews/select_license";

        jassert (stateToUpdate.authToken.isNotEmpty());
        jassert (stateToUpdate.type != LicenseState::Type::notLoggedIn);

        auto accessTokenHeader = "x-access-token: " + stateToUpdate.authToken;
        StringArray licenses;

        while ((licenses.isEmpty() || selectNewLicense) && ! threadShouldExit())
        {
            static Identifier licenseTypeIdentifier   ("type");
            static Identifier licenseStatusIdentifier ("status");
            static Identifier projucerLicenseTypeIdentifier ("licence_type");
            static Identifier productNameIdentifier ("product_name");
            static Identifier licenseIdentifier ("licence");
            static Identifier serialIdentifier  ("serial_number");
            static Identifier versionIdentifier ("product_version");
            static Identifier searchInternalIdentifier ("search_internal_id");

            if (! selectNewLicense)
            {
                ScopedPointer<WebInputStream> shared = getSharedWebInputStream (URL ("https://api.roli.com/api/v1/user/licences?search_internal_id=com.roli.projucer&version=5"),
                                                                                false);
                if (shared == nullptr)
                    break;

                var json = JSON::parse (shared->withExtraHeaders (accessTokenHeader)
                                          .readEntireStreamAsString());
                shared = nullptr;

                if (auto* jsonLicenses = json.getArray())
                {
                    for (auto& v : *jsonLicenses)
                    {
                        if (auto* obj = v.getDynamicObject())
                        {
                            const String& productType = obj->getProperty (projucerLicenseTypeIdentifier);
                            const String& status      = obj->getProperty (licenseStatusIdentifier);

                            if (productType.isNotEmpty() && (status.isEmpty() || status == "active"))
                                licenses.add (productType);
                        }
                    }
                }
                else
                {
                    // no internet -> then use the last valid license
                    if (stateToUpdate.type != LicenseState::Type::notLoggedIn
                     && stateToUpdate.type != LicenseState::Type::noLicenseChosenYet)
                        return;
                }

                if (! licenses.isEmpty())
                    break;
            }

            // ask the user to select a license
            HashMap<String, String> result;
            requiredWebview = true;

            if (! queryWebview (licenseChooserPage, {}, result))
                break;

            const String& redirectURL       = result["page-redirect"];
            const String& productKey        = result["register-product"];
            const String& chosenLicenseType = result["redeem-licence-type"];

            if (redirectURL.isNotEmpty())
            {
                licenseChooserPage = "https://juce.com/webviews/register-product";
                continue;
            }

            if (productKey.isNotEmpty())
            {
                DynamicObject::Ptr redeamObject = new DynamicObject();
                redeamObject->setProperty (serialIdentifier, productKey);

                String postData (JSON::toString (var (redeamObject.get())));

                ScopedPointer<WebInputStream> shared = getSharedWebInputStream (URL ("https://api.roli.com/api/v1/user/products").withPOSTData (postData),
                                                                                true);
                if (shared == nullptr)
                    break;

                int statusCode = shared->withExtraHeaders (accessTokenHeader)
                                        .withExtraHeaders ("Content-Type: application/json")
                                        .getStatusCode();

                licenseChooserPage =  String ("https://juce.com/webviews/register-product?error=")
                                    + String (statusCode == 404 ? "invalid" : "server");

                if (statusCode == 200)
                    selectNewLicense = false;

                continue;
            }

            if (chosenLicenseType.isNotEmpty())
            {
                // redeem the license
                DynamicObject::Ptr jsonLicenseObject = new DynamicObject();
                jsonLicenseObject->setProperty (projucerLicenseTypeIdentifier, chosenLicenseType);
                jsonLicenseObject->setProperty (versionIdentifier,     5);


                DynamicObject::Ptr jsonLicenseRequest = new DynamicObject();
                jsonLicenseRequest->setProperty (licenseIdentifier, var (jsonLicenseObject.get()));
                jsonLicenseRequest->setProperty (searchInternalIdentifier, "com.roli.projucer");
                jsonLicenseRequest->setProperty (licenseTypeIdentifier, "software");

                String postData (JSON::toString (var (jsonLicenseRequest.get())));
                ScopedPointer<WebInputStream> shared
                    = getSharedWebInputStream (URL ("https://api.roli.com/api/v1/user/products/redeem").withPOSTData (postData), true);

                if (shared != nullptr)
                {
                    int statusCode = shared->withExtraHeaders (accessTokenHeader)
                                            .withExtraHeaders ("Content-Type: application/json")
                                            .getStatusCode();

                    if (statusCode == 200)
                        selectNewLicense = false;

                    continue;
                }
            }

            break;
        }

        HashMap<String, String> result;

        if (requiredWebview && ! threadShouldExit())
            queryWebview ("https://juce.com/webviews/registration-complete", "licence_provisioned", result);

        stateToUpdate.type = getBestLicenseTypeFromLicenses (licenses);
    }

    //==============================================================================
    void run() override
    {
        LicenseState workState (owner.state);

        while (! threadShouldExit())
        {
            workState.authToken = getAuthToken();

            if (workState.authToken.isEmpty())
                return;

            // read the user information
            updateUserInfo (workState);

            if (threadShouldExit())
                return;

            updateIfChanged (workState);

            // if the last step logged us out then retry
            if (workState.authToken.isEmpty())
                continue;

            // check if the license has changed
            updateLicenseType (workState);

            if (threadShouldExit())
                return;

            updateIfChanged (workState);
            closeWebviewOnMessageThread (0);
            finished.wait (60 * 5 * 1000);
        }
    }

    //==============================================================================
    LicenseState licenseStateFromJSON (const var& json, const String& authToken, const Image& fallbackAvatar)
    {
        static Identifier usernameIdentifier  ("username");
        static Identifier emailIdentifier     ("email");
        static Identifier avatarURLIdentifier ("avatar_url");

        LicenseState result;

        if (auto* obj = json.getDynamicObject())
        {
            result.type      = LicenseState::Type::noLicenseChosenYet;
            result.username  = obj->getProperty (usernameIdentifier);
            result.authToken = authToken;
            result.email     = obj->getProperty (emailIdentifier);
            result.avatar    = fallbackAvatar;

            String avatarURL = obj->getProperty (avatarURLIdentifier);

            if (avatarURL.isNotEmpty())
            {
                if (ScopedPointer<WebInputStream> shared = getSharedWebInputStream (URL (avatarURL), false))
                {
                    MemoryBlock mb;
                    shared->readIntoMemoryBlock (mb);

                    result.avatar = ImageFileFormat::loadFrom (mb.getData(), mb.getSize());
                }
            }
        }

        return result;
    }

    //==============================================================================
    bool queryWebview (const String& startURL, const String& valueToQuery, HashMap<String, String>& result)
    {
        executeOnMessageThreadAndBlock ([&] () { owner.queryWebview (startURL, valueToQuery, result); }, false);
        return (! threadShouldExit());
    }

    void closeWebviewOnMessageThread (int result)
    {
        executeOnMessageThreadAndBlock ([this, result] () { owner.closeWebview (result); });
    }

    static bool stringArrayContainsSubstring (const StringArray& stringArray, const String& substring)
    {
        jassert (substring.isNotEmpty());

        for (auto element : stringArray)
            if (element.containsIgnoreCase (substring))
                return true;

        return false;
    }

    static LicenseState::Type getBestLicenseTypeFromLicenses (const StringArray& licenses)
    {
        if      (stringArrayContainsSubstring (licenses, "juce-pro"))       return LicenseState::Type::pro;
        else if (stringArrayContainsSubstring (licenses, "juce-indie"))     return LicenseState::Type::indie;
        else if (stringArrayContainsSubstring (licenses, "juce-personal"))  return LicenseState::Type::personal;
        else if (stringArrayContainsSubstring (licenses, "juce-edu"))       return LicenseState::Type::edu;

        return LicenseState::Type::noLicenseChosenYet;
    }

    void updateIfChanged (const LicenseState& newState)
    {
        LicenseState updatedState (owner.state);
        bool changed = false;
        bool shouldUpdateLicenseType = (newState.type     != LicenseState::Type::noLicenseChosenYet
                                     || updatedState.type == LicenseState::Type::notLoggedIn);

        if (newState.type != LicenseState::Type::notLoggedIn) updatedState.avatar = newState.avatar;

        if (owner.state.type      != newState.type && shouldUpdateLicenseType) { updatedState.type      = newState.type;      changed = true; }
        if (owner.state.authToken != newState.authToken)                       { updatedState.authToken = newState.authToken; changed = true; }
        if (owner.state.username  != newState.username)                        { updatedState.username  = newState.username;  changed = true; }
        if (owner.state.email     != newState.email)                           { updatedState.email     = newState.email;     changed = true; }
        if (owner.state.avatar.isValid() != newState.avatar.isValid())         { changed = true; }

        if (changed)
            executeOnMessageThreadAndBlock ([this, updatedState]() { owner.updateState (updatedState); });
    }

    //==============================================================================
    LicenseController& owner;
    bool selectNewLicense;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LicenseThread)
};
