package com.warren.iwanttoheal;

import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import com.getcapacitor.BridgeActivity;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public abstract class ControllerBridgeActivity extends BridgeActivity {

    public static final String EXTRA_INITIAL_URL = "com.warren.iwanttoheal.INITIAL_URL";
    private static final String PREFS_NAME = "com.warren.iwanttoheal.performance";
    private static final String CACHE_CLEARED_VERSION_KEY = "webview_cache_cleared_version";
    private static final float AXIS_DEAD_ZONE = 0.45f;
    private static final long DPAD_THROTTLE_MS = 55;
    private final long[] lastDpadDispatchAt = new long[16];
    private final Set<String> activeMotionTokens = new HashSet<>();
    private final Map<String, Long> lastMotionDispatchAt = new HashMap<>();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        boolean shouldClearWebViewCache = shouldClearWebViewCacheForCurrentVersion();
        if (shouldClearWebViewCache) clearWebViewServiceWorkers();
        super.onCreate(savedInstanceState);
        AndroidDisplayPerformance.preferBatteryRefreshRate(getWindow(), getWindowManager().getDefaultDisplay());
        if (bridge != null) {
            if (shouldClearWebViewCache) {
                bridge.getWebView().clearCache(true);
                markWebViewCacheClearedForCurrentVersion();
            }
        }
        loadIntentUrl();
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
        loadIntentUrl();
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
            enableImmersiveMode();
            AndroidDisplayPerformance.preferBatteryRefreshRate(
                getWindow(),
                getWindowManager().getDefaultDisplay()
            );
        }
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (
            event.getActionMasked() == MotionEvent.ACTION_DOWN
                && bridge != null
                && bridge.getWebView() != null
        ) {
            bridge.getWebView().requestFocus();
        }
        return super.dispatchTouchEvent(event);
    }

    private void loadIntentUrl() {
        if (bridge == null || getIntent() == null) return;
        String initialUrl = getIntent().getStringExtra(EXTRA_INITIAL_URL);
        if (initialUrl == null || initialUrl.isEmpty()) return;
        bridge.getWebView().post(() -> bridge.getWebView().loadUrl(initialUrl));
    }

    private void enableImmersiveMode() {
        getWindow().getDecorView().setSystemUiVisibility(
            View.SYSTEM_UI_FLAG_FULLSCREEN
                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
        );
    }

    private void clearWebViewServiceWorkers() {
        File webViewData = new File(getApplicationInfo().dataDir, "app_webview");
        deleteIfExists(new File(webViewData, "Default/Service Worker"));
        deleteIfExists(new File(webViewData, "Service Worker"));
    }

    private boolean shouldClearWebViewCacheForCurrentVersion() {
        return performancePreferences().getLong(CACHE_CLEARED_VERSION_KEY, -1L)
            != currentVersionCode();
    }

    private void markWebViewCacheClearedForCurrentVersion() {
        performancePreferences()
            .edit()
            .putLong(CACHE_CLEARED_VERSION_KEY, currentVersionCode())
            .apply();
    }

    private SharedPreferences performancePreferences() {
        return getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
    }

    private long currentVersionCode() {
        try {
            PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                return packageInfo.getLongVersionCode();
            }
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException exception) {
            return -1L;
        }
    }

    private void deleteIfExists(File file) {
        if (!file.exists()) return;
        if (file.isDirectory()) {
            File[] children = file.listFiles();
            if (children != null) {
                for (File child : children) {
                    deleteIfExists(child);
                }
            }
        }
        file.delete();
    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        String token = controllerToken(event.getKeyCode());
        if (token == null || bridge == null) return super.dispatchKeyEvent(event);

        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            boolean repeat = event.getRepeatCount() > 0;
            if (isDpadToken(token) && shouldThrottleDpad(token)) return true;
            dispatchNativeControllerToken(token, repeat);
        }
        return true;
    }

    @Override
    public boolean dispatchGenericMotionEvent(MotionEvent event) {
        if (
            bridge == null
                || event.getActionMasked() != MotionEvent.ACTION_MOVE
                || !isControllerMotionEvent(event)
        ) {
            return super.dispatchGenericMotionEvent(event);
        }

        Set<String> currentTokens = new HashSet<>();
        float leftStickX = event.getAxisValue(MotionEvent.AXIS_X);
        float leftStickY = event.getAxisValue(MotionEvent.AXIS_Y);
        dispatchNativeControllerMotion(leftStickX, leftStickY);
        addAxisTokens(
            currentTokens,
            event.getAxisValue(MotionEvent.AXIS_HAT_X),
            "Button14",
            "Button15"
        );
        addAxisTokens(
            currentTokens,
            event.getAxisValue(MotionEvent.AXIS_HAT_Y),
            "Button12",
            "Button13"
        );
        addAxisTokens(
            currentTokens,
            leftStickX,
            "Axis0-",
            "Axis0+"
        );
        addAxisTokens(
            currentTokens,
            leftStickY,
            "Axis1-",
            "Axis1+"
        );

        boolean hadMotionTokens = !activeMotionTokens.isEmpty();
        long now = SystemClock.uptimeMillis();
        for (String token : currentTokens) {
            boolean repeat = activeMotionTokens.contains(token);
            long lastDispatchAt = lastMotionDispatchAt.containsKey(token)
                ? lastMotionDispatchAt.get(token)
                : 0L;
            if (!repeat || now - lastDispatchAt >= DPAD_THROTTLE_MS) {
                dispatchNativeControllerToken(token, repeat);
                lastMotionDispatchAt.put(token, now);
            }
        }
        activeMotionTokens.clear();
        activeMotionTokens.addAll(currentTokens);

        if (currentTokens.isEmpty() && !hadMotionTokens) {
            return super.dispatchGenericMotionEvent(event);
        }
        return true;
    }

    private boolean isControllerMotionEvent(MotionEvent event) {
        int source = event.getSource();
        return (source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
            || (source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD
            || (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
    }

    private void addAxisTokens(
        Set<String> tokens,
        float value,
        String negativeToken,
        String positiveToken
    ) {
        if (value <= -AXIS_DEAD_ZONE) tokens.add(negativeToken);
        if (value >= AXIS_DEAD_ZONE) tokens.add(positiveToken);
    }

    private void dispatchNativeControllerToken(String token, boolean repeat) {
        if (bridge == null || bridge.getWebView() == null) return;
        String script =
            "window.dispatchEvent(new CustomEvent('ashen-halls-native-controller',"
                + "{detail:{token:'" + token + "',repeat:" + repeat + "}}));";
        bridge.getWebView().post(
            () -> {
                bridge.getWebView().requestFocus();
                bridge.getWebView().evaluateJavascript(script, null);
            }
        );
    }

    private void dispatchNativeControllerMotion(float x, float y) {
        if (bridge == null || bridge.getWebView() == null) return;
        String script =
            "window.dispatchEvent(new CustomEvent('ashen-halls-native-controller-motion',"
                + "{detail:{x:" + x + ",y:" + y + "}}));";
        bridge.getWebView().post(
            () -> {
                bridge.getWebView().requestFocus();
                bridge.getWebView().evaluateJavascript(script, null);
            }
        );
    }

    private boolean shouldThrottleDpad(String token) {
        int buttonIndex = Integer.parseInt(token.substring("Button".length()));
        long now = SystemClock.uptimeMillis();
        if (now - lastDpadDispatchAt[buttonIndex] < DPAD_THROTTLE_MS) return true;
        lastDpadDispatchAt[buttonIndex] = now;
        return false;
    }

    private boolean isDpadToken(String token) {
        return token.equals("Button12")
            || token.equals("Button13")
            || token.equals("Button14")
            || token.equals("Button15");
    }

    private String controllerToken(int keyCode) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_BUTTON_A:
            case KeyEvent.KEYCODE_DPAD_CENTER:
            case KeyEvent.KEYCODE_ENTER:
            case KeyEvent.KEYCODE_NUMPAD_ENTER:
                return "Button0";
            case KeyEvent.KEYCODE_BUTTON_B:
            case KeyEvent.KEYCODE_BACK:
            case KeyEvent.KEYCODE_ESCAPE:
                return "Button1";
            case KeyEvent.KEYCODE_BUTTON_X:
                return "Button2";
            case KeyEvent.KEYCODE_BUTTON_Y:
                return "Button3";
            case KeyEvent.KEYCODE_BUTTON_L1:
                return "Button4";
            case KeyEvent.KEYCODE_BUTTON_R1:
                return "Button5";
            case KeyEvent.KEYCODE_BUTTON_L2:
                return "Button6";
            case KeyEvent.KEYCODE_BUTTON_R2:
                return "Button7";
            case KeyEvent.KEYCODE_BUTTON_SELECT:
                return "Button8";
            case KeyEvent.KEYCODE_BUTTON_START:
                return "Button9";
            case KeyEvent.KEYCODE_BUTTON_THUMBL:
                return "Button10";
            case KeyEvent.KEYCODE_BUTTON_THUMBR:
                return "Button11";
            case KeyEvent.KEYCODE_DPAD_UP:
                return "Button12";
            case KeyEvent.KEYCODE_DPAD_DOWN:
                return "Button13";
            case KeyEvent.KEYCODE_DPAD_LEFT:
                return "Button14";
            case KeyEvent.KEYCODE_DPAD_RIGHT:
                return "Button15";
            default:
                return null;
        }
    }
}
