/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <Launcher_precompiled.h>
#include <Launcher.h>

#include <../Common/UnixLike/Launcher_UnixLike.h>

#include <AzCore/Android/AndroidEnv.h>
#include <AzCore/Android/Utils.h>
#include <AzCore/Android/JNI/JNI.h>
#include <AzCore/Android/JNI/Object.h>
#include <AzCore/Android/JNI/scoped_ref.h>

#include <AzFramework/API/ApplicationAPI_Platform.h>
#include <AzFramework/Input/Buses/Notifications/RawInputNotificationBus_Platform.h>
#include <AzGameFramework/Application/GameApplication.h>

#include <IConsole.h>

#include <android/asset_manager_jni.h>
#include <android/log.h>
#include <android/native_activity.h>
#include <android/native_window.h>

#include <android_native_app_glue.h>

#include <sys/resource.h>
#include <sys/types.h>


#if defined(ENABLE_LOGGING)
    #define LOG_TAG "LMBR"
    #define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__))
    #define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__))
    #define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))

    struct COutputPrintSink
        : public IOutputPrintSink
    {
        virtual void Print(const char* message)
        {
            LOGI("%s", message);
        }
    };

    COutputPrintSink g_androidPrintSink;
#else
    #define LOGI(...)
    #define LOGE(...)
#endif // !defined(_RELEASE)


#define MAIN_EXIT_FAILURE(_appState, ...) \
    LOGE("****************************************************************"); \
    LOGE("STARTUP FAILURE - EXITING"); \
    LOGE("REASON:"); \
    LOGE(__VA_ARGS__); \
    LOGE("****************************************************************"); \
    _appState->userData = nullptr; \
    ANativeActivity_finish(_appState->activity); \
    while (_appState->destroyRequested == 0) { \
        g_eventDispatcher.PumpAllEvents(); \
    } \
    return;


namespace
{
    class NativeEventDispatcher
        : public AzFramework::AndroidEventDispatcher
    {
    public:
        NativeEventDispatcher()
            : m_appState(nullptr)
        {
        }

        ~NativeEventDispatcher() = default;

        void PumpAllEvents() override
        {
            bool continueRunning = true;
            while (continueRunning) 
            {
                continueRunning = PumpEvents(&ALooper_pollAll);
            }
        }

        void PumpEventLoopOnce() override
        {
            PumpEvents(&ALooper_pollOnce);
        }

        void SetAppState(android_app* appState)
        {
            m_appState = appState;
        }

    private:
        // signature of ALooper_pollOnce and ALooper_pollAll -> int timeoutMillis, int* outFd, int* outEvents, void** outData
        typedef int (*EventPumpFunc)(int, int*, int*, void**); 

        bool PumpEvents(EventPumpFunc looperFunc)
        {
            if (!m_appState)
            {
                return false;
            }

            int events = 0;
            android_poll_source* source = nullptr;
            const AZ::Android::AndroidEnv* androidEnv = AZ::Android::AndroidEnv::Get();

            // when timeout is negative, the function will block until an event is received
            const int result = looperFunc(androidEnv->IsRunning() ? 0 : -1, nullptr, &events, reinterpret_cast<void**>(&source));

            // the value returned from the looper poll func is either:
            // 1. the identifier associated with the event source (>= 0) and has event data that needs to be processed manually
            // 2. an ALOOPER_POLL_* enum (< 0) indicating there is no data to be processed due to error or callback(s) registered 
            //    with the event source were called
            const bool validIdentifier = (result >= 0);
            if (validIdentifier && source)
            {
                source->process(m_appState, source);
            }

            const bool destroyRequested = (m_appState->destroyRequested != 0);
            if (destroyRequested)
            {
                AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::ExitMainLoop);
            }

            return (validIdentifier && !destroyRequested);
        }

        android_app* m_appState;
    };


    NativeEventDispatcher g_eventDispatcher;
    bool g_windowInitialized = false;


    void OnPostAppStart()
    {
        // set the event dispatcher with the application framework
        AzFramework::AndroidAppRequests::Bus::Broadcast(&AzFramework::AndroidAppRequests::SetEventDispatcher, &g_eventDispatcher);

        // queue the dismissal of the system splash screen in case the engine splash is disabled
        AZ::TickBus::QueueFunction([](){
            AZ::Android::Utils::DismissSplashScreen();
        });
    }

    int32_t HandleInputEvents(android_app* app, AInputEvent* event)
    {
        AzFramework::RawInputNotificationBusAndroid::Broadcast(&AzFramework::RawInputNotificationsAndroid::OnRawInputEvent, event);
        return 0;
    }

    void HandleApplicationLifecycleEvents(android_app* appState, int32_t command)
    {
    #if defined(ENABLE_LOGGING)
        const char* commandNames[] = {
            "APP_CMD_INPUT_CHANGED",
            "APP_CMD_INIT_WINDOW",
            "APP_CMD_TERM_WINDOW",
            "APP_CMD_WINDOW_RESIZED",
            "APP_CMD_WINDOW_REDRAW_NEEDED",
            "APP_CMD_CONTENT_RECT_CHANGED",
            "APP_CMD_GAINED_FOCUS",
            "APP_CMD_LOST_FOCUS",
            "APP_CMD_CONFIG_CHANGED",
            "APP_CMD_LOW_MEMORY",
            "APP_CMD_START",
            "APP_CMD_RESUME",
            "APP_CMD_SAVE_STATE",
            "APP_CMD_PAUSE",
            "APP_CMD_STOP",
            "APP_CMD_DESTROY",
        };
        if (command >= 0 && command < sizeof(commandNames))
        {
            LOGI("Engine command received: %s", commandNames[command]);
        }
        else
        {
            LOGW("Unknown engine command received: %d", command);
        }
    #endif

        AZ::Android::AndroidEnv* androidEnv = static_cast<AZ::Android::AndroidEnv*>(appState->userData);
        if (!androidEnv)
        {
            return;
        }

        switch (command)
        {
            case APP_CMD_GAINED_FOCUS:
            {
                AzFramework::AndroidLifecycleEvents::Bus::Broadcast(
                    &AzFramework::AndroidLifecycleEvents::Bus::Events::OnGainedFocus);
            }
            break;

            case APP_CMD_LOST_FOCUS:
            {
                AzFramework::AndroidLifecycleEvents::Bus::Broadcast(
                    &AzFramework::AndroidLifecycleEvents::Bus::Events::OnLostFocus);
            }
            break;

            case APP_CMD_PAUSE:
            {
                AzFramework::AndroidLifecycleEvents::Bus::Broadcast(
                    &AzFramework::AndroidLifecycleEvents::Bus::Events::OnPause);
                androidEnv->SetIsRunning(false);
            }
            break;

            case APP_CMD_RESUME:
            {
                androidEnv->SetIsRunning(true);
                AzFramework::AndroidLifecycleEvents::Bus::Broadcast(
                    &AzFramework::AndroidLifecycleEvents::Bus::Events::OnResume);
            }
            break;

            case APP_CMD_DESTROY:
            {
                AzFramework::AndroidLifecycleEvents::Bus::Broadcast(
                    &AzFramework::AndroidLifecycleEvents::Bus::Events::OnDestroy);
            }
            break;

            case APP_CMD_INIT_WINDOW:
            {
                g_windowInitialized = true;
                androidEnv->SetWindow(appState->window);

                AzFramework::AndroidLifecycleEvents::Bus::Broadcast(
                    &AzFramework::AndroidLifecycleEvents::Bus::Events::OnWindowInit);
            }
            break;

            case APP_CMD_TERM_WINDOW:
            {
                AzFramework::AndroidLifecycleEvents::Bus::Broadcast(
                    &AzFramework::AndroidLifecycleEvents::Bus::Events::OnWindowDestroy);

                androidEnv->SetWindow(nullptr);
            }
            break;

            case APP_CMD_LOW_MEMORY:
            {
                AzFramework::AndroidLifecycleEvents::Bus::Broadcast(
                    &AzFramework::AndroidLifecycleEvents::Bus::Events::OnLowMemory);
            }
            break;

            case APP_CMD_CONFIG_CHANGED:
            {
                androidEnv->UpdateConfiguration();
            }
            break;

            case APP_CMD_WINDOW_REDRAW_NEEDED:
            {
                AzFramework::AndroidLifecycleEvents::Bus::Broadcast(
                    &AzFramework::AndroidLifecycleEvents::Bus::Events::OnWindowRedrawNeeded);
            }
            break;
        }
    }

    void OnWindowRedrawNeeded(ANativeActivity* activity, ANativeWindow* rect)
    {
        android_app* app = static_cast<android_app*>(activity->instance);
        int8_t cmd = APP_CMD_WINDOW_REDRAW_NEEDED;
        if (write(app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd))
        {
            LOGE("Failure writing android_app cmd: %s\n", strerror(errno));
        }
    }
}

#if AZ_TESTS_ENABLED

void android_main(android_app* appState)
{
    // TODO: Android needs the following data to be resolved amongst other things to support a target unit test Launcher:
    //
    // 1. The path to the running application on the device
    // 2. A way to translate the appState into command line arguments (argv, argc) to pass along to the unit test launcher
    // 3. Determine how to capture the stdout/stderr stream from the unit tests to the console of the android app
    // 4. Update the Module Handler in AzTest to support Android modules
    //

    // LumberyardLauncher::ReturnCode status = LumberyardLauncher::RunUnitTests(executablePath, argv, argc);
    //
    // if (status != ReturnCode::Success)
    // {
    //    MAIN_EXIT_FAILURE(appState, GetReturnCodeString(status));
    //}

    MAIN_EXIT_FAILURE(appState,"The UnitTest Launcher is not support on android.");
}

#else
// This is the main entry point of a native application that is using android_native_app_glue.
// It runs in its own thread, with its own event loop for receiving input events
void android_main(android_app* appState)
{
    // Adding a start up banner so you can see when the game is starting up in amongst the logcat spam
    LOGI("****************************************************************");
    LOGI("*             Amazon Lumberyard - Launching Game...            *");
    LOGI("****************************************************************");

    // setup the system command handler which are guaranteed to be called on the same
    // thread the events are pumped
    appState->onAppCmd = HandleApplicationLifecycleEvents;
    appState->onInputEvent = HandleInputEvents;
    g_eventDispatcher.SetAppState(appState);

    // This callback will notify us when the orientation of the device changes.
    // While Android does have an onNativeWindowResized callback, it is never called in android_native_app_glue when the window size changes.
    // The onNativeConfigChanged callback is called too early(before the window size has changed), so we won't have the correct window size at that point.
    appState->activity->callbacks->onNativeWindowRedrawNeeded = OnWindowRedrawNeeded;

    // setup the android environment
    AZ::AllocatorInstance<AZ::OSAllocator>::Create();
    {
        AZ::Android::AndroidEnv::Descriptor descriptor;

        descriptor.m_jvm = appState->activity->vm;
        descriptor.m_activityRef = appState->activity->clazz;
        descriptor.m_assetManager = appState->activity->assetManager;

        descriptor.m_configuration = appState->config;

        descriptor.m_appPrivateStoragePath = appState->activity->internalDataPath;
        descriptor.m_appPublicStoragePath = appState->activity->externalDataPath;
        descriptor.m_obbStoragePath = appState->activity->obbPath;

        if (!AZ::Android::AndroidEnv::Create(descriptor))
        {
            AZ::Android::AndroidEnv::Destroy();
            AZ::AllocatorInstance<AZ::OSAllocator>::Destroy();
            MAIN_EXIT_FAILURE(appState, "Failed to create the AndroidEnv");
        }

        AZ::Android::AndroidEnv* androidEnv = AZ::Android::AndroidEnv::Get();
        appState->userData = androidEnv;
        androidEnv->SetIsRunning(true);
    }

    // sync the window creation
    while (!g_windowInitialized)
    {
        g_eventDispatcher.PumpAllEvents();
    }

    // Now that the window has been created we can show the java splash screen.  We need
    // to do it here and not in the window init event because every time the app is
    // backgrounded/foregrounded the window is destroyed/created, respectively.  So, we
    // don't want to show the splash screen when we resumed from a paused state.
    AZ::Android::Utils::ShowSplashScreen();

    // run the Lumberyard application
    using namespace LumberyardLauncher;
    
    PlatformMainInfo mainInfo;
    mainInfo.m_updateResourceLimits = IncreaseResourceLimits;
    mainInfo.m_onPostAppStart = OnPostAppStart;
    mainInfo.m_appResourcesPath = AZ::Android::Utils::FindAssetsDirectory();
    mainInfo.m_additionalVfsResolution = "\t- Make sure \'adb reverse\' is setup for the device when connecting to localhost";

    // Always add the app as the first arg to mimic the way other platforms start with the executable name.
    const char* packageName = AZ::Android::Utils::GetPackageName();
    if (packageName)
    {
        mainInfo.AddArgument(packageName);
    }

    // Get the string extras and pass them along as cmd line params
    AZ::Android::JNI::Internal::Object<AZ::OSAllocator> activityObject(AZ::Android::JNI::GetEnv()->GetObjectClass(appState->activity->clazz), appState->activity->clazz);

    activityObject.RegisterMethod("getIntent", "()Landroid/content/Intent;");
    jobject intent = activityObject.InvokeObjectMethod<jobject>("getIntent");

    AZ::Android::JNI::Internal::Object<AZ::OSAllocator> intentObject(AZ::Android::JNI::GetEnv()->GetObjectClass(intent), intent);
    intentObject.RegisterMethod("getStringExtra", "(Ljava/lang/String;)Ljava/lang/String;");
    intentObject.RegisterMethod("getExtras", "()Landroid/os/Bundle;");
    jobject extras = intentObject.InvokeObjectMethod<jobject>("getExtras");

    if (extras)
    {
        // Get the set of keys
        AZ::Android::JNI::Internal::Object<AZ::OSAllocator> extrasObject(AZ::Android::JNI::GetEnv()->GetObjectClass(extras), extras);
        extrasObject.RegisterMethod("keySet", "()Ljava/util/Set;");
        jobject extrasKeySet = extrasObject.InvokeObjectMethod<jobject>("keySet");

        // get the array of string objects
        AZ::Android::JNI::Internal::Object<AZ::OSAllocator> extrasKeySetObject(AZ::Android::JNI::GetEnv()->GetObjectClass(extrasKeySet), extrasKeySet);
        extrasKeySetObject.RegisterMethod("toArray", "()[Ljava/lang/Object;");
        jobjectArray extrasKeySetArray = extrasKeySetObject.InvokeObjectMethod<jobjectArray>("toArray");

        int extrasKeySetArraySize = AZ::Android::JNI::GetEnv()->GetArrayLength(extrasKeySetArray);

        for (int x = 0; x < extrasKeySetArraySize; x++)
        {
            jstring keyObject = static_cast<jstring>(AZ::Android::JNI::GetEnv()->GetObjectArrayElement(extrasKeySetArray, x));
            AZ::OSString value = intentObject.InvokeStringMethod("getStringExtra", keyObject);

            const char* keyChars = AZ::Android::JNI::GetEnv()->GetStringUTFChars(keyObject, 0);

            char argName[AZ_COMMAND_LINE_LEN] = { 0 };
            azsprintf(argName, "-%s", keyChars);
            mainInfo.AddArgument(argName);
            mainInfo.AddArgument(value.c_str());

            AZ::Android::JNI::GetEnv()->ReleaseStringUTFChars(keyObject, keyChars);
        }
    }

#if defined(_RELEASE)
    mainInfo.m_appWriteStoragePath = AZ::Android::Utils::GetAppPrivateStoragePath();
#else
    mainInfo.m_appWriteStoragePath = AZ::Android::Utils::GetAppPublicStoragePath();
#endif // defined(_RELEASE)
    
#if defined(ENABLE_LOGGING)
    mainInfo.m_printSink = &g_androidPrintSink;
#endif // defined(ENABLE_LOGGING)

    ReturnCode status = Run(mainInfo);

    AZ::Android::AndroidEnv::Destroy();
    AZ::AllocatorInstance<AZ::OSAllocator>::Destroy();

    if (status != ReturnCode::Success)
    {
        MAIN_EXIT_FAILURE(appState, GetReturnCodeString(status));
    }
}

#endif // AZ_TESTS_ENABLED