//
// Created by kevin on 02.10.2022.
//

#include "NativeEngine.h"
#include "Log.h"
#include "AndroidAssetManager.h"
#include "Renderer.h"
#include "TouchInput.h"
/*#include "Game.h"
#include "base.h"*/

#include <assert.h>
#include <stdlib.h>
#include <swappy/swappyGL.h>
#include <GLES3/gl3.h>

static GameState g_app_state = {false};

static void _handle_cmd_proxy(struct android_app* app, int32_t cmd)
{
    auto* engine = reinterpret_cast<NativeEngine*>(app->userData);
    engine->handleAppCmd(cmd);
}

NativeEngine::NativeEngine(struct android_app* app)
    : m_app(app),
      m_egl_display(EGL_NO_DISPLAY),
      m_egl_surface(EGL_NO_SURFACE),
      m_egl_context(EGL_NO_CONTEXT),
      m_jni_env(nullptr),
      m_has_focus(false),
      m_has_window(false),
      m_is_visible(false),
      m_in_motion(false)
{
    SwappyGL_init(getJniEnv(), m_app->activity->javaGameActivity);
    SwappyGL_setSwapIntervalNS(SWAPPY_SWAP_20FPS);

    android_app_set_key_event_filter(m_app, NULL);
    android_app_set_motion_event_filter(m_app, NULL);

    AndroidAssetManager::create(m_app->activity->assetManager);

    // INTEGRATION
    // Hier kannst du Initialisierungscode aufrufen.
    // Beachte das Renderer noch _nicht_ gültig ist.
    // Der Renderer wird erst während des ersten Frames erzeugt.
    //
    // Du kannst schon Assets laden, da oben der AssetManager erzeugt wurde.

    // Ende INTEGRATION

    // BEISPIELCODE
    m_smiley = StringRepository::global->internString("smiley_PNG42.png");
    // m_smiley_pos = { 100, 100 };
    //  ENDE VOM BEISPIELCODE
}

NativeEngine::~NativeEngine()
{
    ALOGI("NativeEngine: destructor");

    // INTEGRATION
    // Hier könntest du eine "onExit" funktion aufrufen
    //

    SwappyGL_destroy();
    killContext();
    if (m_jni_env) {
        m_app->activity->vm->DetachCurrentThread();
    }
}

void NativeEngine::gameLoop()
{
    m_app->userData = this;
    m_app->onAppCmd = _handle_cmd_proxy;
    m_app->textInputState = 0;

    while (true) {
        int events;
        struct android_poll_source* source;

        // Poll commands
        while ((ALooper_pollAll(isAnimating() ? 0 : -1, nullptr, &events, reinterpret_cast<void**>(&source))) >= 0) {
            if (source)
                source->process(source->app, source);
            if (m_app->destroyRequested)
                return;
        }

        constexpr int MAX_INPUT_EVENTS = 16;
        TouchInputEvent input_events[MAX_INPUT_EVENTS];
        int input_event_count = 0;

        // Handle input
        android_input_buffer* input_buffer = android_app_swap_input_buffers(m_app);
        if (input_buffer) {
            if (input_buffer->keyEventsCount > 0) {
                android_app_clear_key_events(input_buffer);
            }
            if (input_buffer->motionEventsCount > 0) {
                float rel_size = m_display_height;
                if (m_display_height > m_display_width) {
                    rel_size = m_display_width;
                }
                rel_size *= 0.04f;
                // float rel_size = MIN(m_display_height, m_display_width) * 0.04f;
                for (unsigned int i = 0; i < input_buffer->motionEventsCount; ++i) {
                    GameActivityMotionEvent motion_event = input_buffer->motionEvents[i];
                    if (motion_event.action == AMOTION_EVENT_ACTION_DOWN) {
                        if (motion_event.pointerCount > 0) {
                            GameActivityPointerAxes pointer = motion_event.pointers[0];
                            float x = GameActivityPointerAxes_getX(&pointer);
                            float y = GameActivityPointerAxes_getY(&pointer);

                            // Record this motion
                            m_in_motion = true;
                            m_last_down.x = x;
                            m_last_down.y = y;
                        }
                    }
                    else if (motion_event.action == AMOTION_EVENT_ACTION_MOVE) {
                        if (motion_event.pointerCount > 0) {
                            GameActivityPointerAxes pointer = motion_event.pointers[0];
                            float x = GameActivityPointerAxes_getX(&pointer);
                            float y = GameActivityPointerAxes_getY(&pointer);

                            if (!m_in_motion) {
                                // Record this motion
                                m_in_motion = true;
                                m_last_down.x = x;
                                m_last_down.y = y;
                            }

                            float dist = sqrtf((x - m_last_down.x) * (x - m_last_down.x) +
                                               (y - m_last_down.y) * (y - m_last_down.y));
                            if (dist >= rel_size) {
                                input_events[input_event_count].kind = TouchInputEventKind::InSwipe;
                                input_events[input_event_count].start = {m_last_down.x, m_last_down.y};
                                input_events[input_event_count].end = {x, y};
                                ++input_event_count;
                            }
                        }
                    }
                    else if (motion_event.action == AMOTION_EVENT_ACTION_UP) {
                        if (!m_in_motion) {
                            ALOGW("Got an UP motion without a corresponding DOWN motion");
                            continue;
                        }

                        if (motion_event.pointerCount > 0) {
                            GameActivityPointerAxes pointer = motion_event.pointers[0];
                            float x = GameActivityPointerAxes_getX(&pointer);
                            float y = GameActivityPointerAxes_getY(&pointer);

                            float dist = sqrtf((x - m_last_down.x) * (x - m_last_down.x) +
                                               (y - m_last_down.y) * (y - m_last_down.y));
                            if (dist < rel_size) {
                                // TAP
                                ALOGI("TAP at %f %f", x, y);
                                input_events[input_event_count].kind = TouchInputEventKind::Tap;
                                input_events[input_event_count].start = {x, y};
                                input_events[input_event_count].end = {x, y};
                                ++input_event_count;
                            }
                            else {
                                // Swipe
                                ALOGI("Swipe from %f %f to %f %f", m_last_down.x, m_last_down.y, x, y);
                                input_events[input_event_count].kind = TouchInputEventKind::EndSwipe;
                                input_events[input_event_count].start = {m_last_down.x, m_last_down.y};
                                input_events[input_event_count].end = {x, y};
                                ++input_event_count;
                            }
                            m_in_motion = false;
                            if (input_event_count == MAX_INPUT_EVENTS)
                                break;
                        }
                    }
                    else if (motion_event.action == AMOTION_EVENT_ACTION_CANCEL) {
                        m_in_motion = false;
                    }
                }
                android_app_clear_motion_events(input_buffer);
            }
        }
        else {
            m_in_motion = false;
        }

        if (isAnimating() && Renderer::ptr) {
            // INTEGRATION Rufe hier deine "gameloop"/"update" funktion auf, die als Parameter
            // den GameState, input_events und input_event_count und die Displaygröße bekommen sollte:
            //
            // kUpdate(&m_state, input_events, input_event_count,
            //         0.0,
            //         nullptr, 0,
            //         nullptr, 0,
            //         m_display_width, m_display_height)
            //
            // (Für Kompatibilität zu Windows sollte die funktion auch Tastatur und Scroll input akzeptieren,
            // den es unter Android aber momentan nicht gibt.)
            //
            // Die Funktion könnte folgende Definition haben:
            // void kUpdate(GameState* state,
            //              const TouchInputEvent* touch_events,
            //              unsigned int touch_event_count,
            //              double scroll_offset,
            //              const unsigned int* pressed_codepoints,
            //              unsigned int codepoint_count,
            //              const int* pressed_keys,
            //              unsigned int pressed_key_count,
            //              float display_width,
            //              float display_height)
            // ENDE INTEGRATION

            // BEISPIELCODE
            static float x = 1.f;
            static float d = -0.01f;
            x += d;
            if (x <= 0.f)
                d *= -1.f;
            else if (x >= 1.f)
                d *= -1.f;

            if (input_event_count > 0) {
                m_smiley_pos = input_events[input_event_count - 1].end;
                m_smiley_pos.x -= 250;
                m_smiley_pos.y -= 250;
            }

            Renderer::ptr->addRect(100, 100, 500, 500, 0.3f, 0.3f, 0.3f, 1.f);
            Renderer::ptr->addRect(m_smiley_pos.x, m_smiley_pos.y, 500, 500, 0.f, x * x, 1.f - x * x, 1.f, m_smiley);
            // ENDE VOM BEISPIELCODE
        }

        if (isAnimating()) {
            renderFrame();
        }
    }
}

bool NativeEngine::isAnimating() const
{
    return m_has_window && m_has_focus && m_is_visible;
}

void NativeEngine::handleAppCmd(int32_t cmd)
{
    ALOGV("NativeEngine: Handling command %d.", cmd);
    switch (cmd) {
    case APP_CMD_SAVE_STATE:
        ALOGV("NativeEngine:: APP_CMD_SAVE_STATE");
        m_state.has_focus = m_has_focus;
        m_app->savedState = malloc(sizeof(m_state));
        *reinterpret_cast<GameState*>(m_app->savedState) = m_state;
        m_app->savedStateSize = sizeof(m_state);
        break;
    case APP_CMD_INIT_WINDOW:
        ALOGV("NativeEngine:: APP_CMD_INIT_WINDOW");
        if (m_app->window) {
            m_has_window = true;
            SwappyGL_setWindow(m_app->window);
            if (m_app->savedStateSize == sizeof(m_state) && m_app->savedState) {
                m_state = *reinterpret_cast<GameState*>(m_app->savedState);
                m_has_focus = m_state.has_focus;
            }
            else {
                // Workaround APP_CMD_GAINED_FOCUS issue where the focus state is not passed
                // down from GameActivity when restarting Activity
                m_has_focus = g_app_state.has_focus;
            }
        }
        ALOGV("HandleCommand(%d): hasWindow = %d, hasFocus = %d", cmd, m_has_window ? 1 : 0, m_has_focus ? 1 : 0);
        break;
    case APP_CMD_TERM_WINDOW:
        ALOGV("NativeEngine: APP_CMD_TERM_WINDOW");
        killSurface();
        m_has_window = false;
        break;
    case APP_CMD_GAINED_FOCUS:
        ALOGV("NativeEngine: APP_CMD_GAINED_FOCUS");
        m_has_focus = true;
        m_state.has_focus = g_app_state.has_focus = m_has_focus;
        break;
    case APP_CMD_LOST_FOCUS:
        ALOGV("NativeEngine: APP_CMD_LOST_FOCUS");
        m_has_focus = false;
        m_state.has_focus = g_app_state.has_focus = m_has_focus;
        break;
    case APP_CMD_START:
        ALOGV("NativeEngine: APP_CMD_START");
        m_is_visible = true;
        break;
    case APP_CMD_STOP:
        ALOGV("NativeEngine: APP_CMD_STOP");
        m_is_visible = false;
        break;
    case APP_CMD_RESUME:
        ALOGV("NativeEngine: APP_CMD_RESUME");
        break;
    default:
        ALOGW("Unhandled command.");
        break;
    }
    ALOGV("NativeEngine STATUS: F%d, V%d, W%d, EGL: D %p, S %p, CTX %p, CFG %p",
          m_has_focus,
          m_is_visible,
          m_has_window,
          m_egl_display,
          m_egl_surface,
          m_egl_context,
          m_egl_config);
}

JNIEnv* NativeEngine::getJniEnv()
{
    if (!m_jni_env) {
        if (m_app->activity->vm->AttachCurrentThread(&m_jni_env, nullptr) != 0) {
            ALOGE("*** FATAL ERROR: failed to attach thread to JNI.");
            exit(1);
        }
        ALOGI("Attached current thread to JNI, %p", m_jni_env);
    }
    return m_jni_env;
}

bool NativeEngine::prepareToRender()
{
    if (m_egl_display == EGL_NO_DISPLAY || m_egl_surface == EGL_NO_SURFACE || m_egl_context == EGL_NO_CONTEXT) {
        if (!initDisplay()) {
            ALOGE("NativeEngine: failed to create display");
            return false;
        }

        if (!initSurface()) {
            ALOGE("NativeEngine: failed to create surface.");
            return false;
        }

        if (!initContext()) {
            ALOGE("NativeEngine: failed to create context.");
            return false;
        }

        ALOGI("NativeEngine: binding surface and context (display %p, surface %p, context %p)",
              m_egl_display,
              m_egl_surface,
              m_egl_context);

        if (eglMakeCurrent(m_egl_display, m_egl_surface, m_egl_surface, m_egl_context) == EGL_FALSE) {
            ALOGE("NativeEngine: eglMakeCurrent failed, EGL error: %d", eglGetError());
            return false;
        }

        initGLObjects();
    }
    return true;
}

bool NativeEngine::initDisplay()
{
    if (m_egl_display != EGL_NO_DISPLAY) {
        return true;
    }

    ALOGI("NativeEngine: Initializing display");
    m_egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if (eglInitialize(m_egl_display, 0, 0) == EGL_FALSE) {
        ALOGE("NativeEngine: Failed to init display, error %d", eglGetError());
        return false;
    }
    return true;
}

void NativeEngine::killDisplay()
{
    ALOGI("NativeEngine: Killing display");
    killContext();
    killSurface();

    if (m_egl_display != EGL_NO_DISPLAY) {
        eglTerminate(m_egl_display);
        m_egl_display = EGL_NO_DISPLAY;
    }
}

bool NativeEngine::initSurface()
{
    assert(m_egl_display != EGL_NO_DISPLAY);
    if (m_egl_surface != EGL_NO_SURFACE) {
        return true;
    }

    ALOGI("NativeEngine: initializing surface");

    EGLint num_configs;
    const EGLint attribs[] = {
        EGL_RENDERABLE_TYPE,
        EGL_OPENGL_ES3_BIT,
        EGL_SURFACE_TYPE,
        EGL_WINDOW_BIT,
        EGL_BLUE_SIZE,
        8,
        EGL_GREEN_SIZE,
        8,
        EGL_RED_SIZE,
        8,
        EGL_DEPTH_SIZE,
        16,
        EGL_NONE,
    };
    eglChooseConfig(m_egl_display, attribs, &m_egl_config, 1, &num_configs);

    m_egl_surface = eglCreateWindowSurface(m_egl_display, m_egl_config, m_app->window, nullptr);
    if (m_egl_surface == EGL_NO_SURFACE) {
        ALOGE("Failed to create EGL surface. EGL error: %d", eglGetError());
        return false;
    }
    return true;
}

void NativeEngine::killSurface()
{
    ALOGI("NativeEngine: Killing surface.");
    eglMakeCurrent(m_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    if (m_egl_surface != EGL_NO_SURFACE) {
        eglDestroySurface(m_egl_display, m_egl_surface);
        m_egl_surface = EGL_NO_SURFACE;
    }
}

bool NativeEngine::initContext()
{
    ALOGI("NativeEngine: initializing context");
    assert(m_egl_display != EGL_NO_DISPLAY);

    EGLint attrib_list[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};

    if (m_egl_context != EGL_NO_CONTEXT)
        return true;

    m_egl_context = eglCreateContext(m_egl_display, m_egl_config, nullptr, attrib_list);
    if (m_egl_context == EGL_NO_CONTEXT) {
        ALOGE("Failed to create EGL context, egl error: %d", eglGetError());
        return false;
    }
    return true;
}

void NativeEngine::killContext()
{
    ALOGI("NativeEngine: Killing context");

    killGLObjects();

    eglMakeCurrent(m_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    if (m_egl_context != EGL_NO_CONTEXT) {
        eglDestroyContext(m_egl_display, m_egl_context);
        m_egl_context = EGL_NO_CONTEXT;
    }
}

bool NativeEngine::initGLObjects()
{
    if (Renderer::ptr) {
        return true;
    }
    Renderer::create();

    return true;
}

void NativeEngine::killGLObjects()
{
    if (Renderer::ptr) {
        Renderer::destroy();
    }
}

void NativeEngine::renderFrame()
{
    if (!prepareToRender()) {
        ALOGE("NativeEngine: preparation to render failed");
        return;
    }

    int width, height;
    eglQuerySurface(m_egl_display, m_egl_surface, EGL_WIDTH, &width);
    eglQuerySurface(m_egl_display, m_egl_surface, EGL_HEIGHT, &height);

    m_display_width = static_cast<float>(width);
    m_display_height = static_cast<float>(height);

    Renderer::ptr->renderFrame(m_display_width, m_display_height);

    if (!SwappyGL_swap(m_egl_display, m_egl_surface)) {
        ALOGW("NativeEngine: SwappyGL_swap failed, EGL error %d", eglGetError());
    }
}