package io.github.madjacket.kat5200;

import android.Manifest;
import android.app.Activity;
import android.app.UiModeManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.Settings;
import android.view.InputDevice;
import android.view.View;
import android.view.WindowInsets;
import java.io.File;
import java.util.ArrayList;
import org.libsdl.app.SDLActivity;

/**
 * kat5200's Android entry point.  SDLActivity (from the SDL3 .aar) loads the
 * libraries listed below in order, then calls SDL_main in the last one
 * (libkat5200.so).  SDL_main is our normal main(), redefined via
 * &lt;SDL3/SDL_main.h&gt; in kat5200.c under __ANDROID__.
 */
public class MainActivity extends SDLActivity {

    /**
     * Receive a UI-thread WindowInsets snapshot in native code.  Native keeps
     * the packed value in atomic state, so the SDL render thread never needs to
     * call back through JNI or access View state while laying out a frame.
     */
    private static native void nativeSetContentInsetsPacked(long packed);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (Build.VERSION.SDK_INT < 30)
            return;

        final View decorView = getWindow().getDecorView();

        /* Activity lifecycle callbacks and insets dispatch both run on the UI
         * thread.  Listen on the decor rather than SDL's SurfaceView so SDL can
         * retain its own insets listener; returning the original object keeps
         * normal child dispatch intact. */
        nativeSetContentInsetsPacked(-1L);
        decorView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
            @Override
            public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
                nativeSetContentInsetsPacked(packContentInsets(windowInsets));
                return windowInsets;
            }
        });

        /* Avoid a one-frame jump when the decor was already attached before
         * our listener was registered, then request future dispatch normally. */
        WindowInsets initialInsets = decorView.getRootWindowInsets();
        if (initialInsets != null)
            nativeSetContentInsetsPacked(packContentInsets(initialInsets));
        decorView.requestApplyInsets();
    }

    /**
     * True when running on Android TV (a leanback / 10-foot device).  Cached on
     * first query so the native side can read it cheaply over JNI
     * (pc_is_android_tv in sdl_if.c) to pick fixed-landscape orientation and a
     * larger UI scale floor.  Default true-unknown is avoided: -1 = not yet
     * resolved, 0 = handheld, 1 = TV.
     */
    private static int sIsTv = -1;

    @Override
    protected String[] getLibraries() {
        return new String[] {
            "SDL3",
            "SDL3_image",
            "kat5200"
        };
    }

    /**
     * Called from native code (sdl_if.c) via JNI.  Returns true on Android TV.
     * Resolves lazily from UiModeManager and caches the result.
     */
    public static boolean isAndroidTv() {
        if (sIsTv < 0) {
            Context ctx = getContext();
            if (ctx == null)
                return false;   /* SDL context not ready: stay unresolved, retry later */
            sIsTv = detectTv(ctx) ? 1 : 0;
        }
        return sIsTv == 1;
    }

    /* Pack the visible system bars and display cutout, deliberately excluding
     * SDL's larger gesture-only regions.  Each component occupies 16 bits in
     * left, top, right, bottom order. */
    private static long packContentInsets(WindowInsets windowInsets) {
        Insets insets = windowInsets.getInsets(
                WindowInsets.Type.statusBars()
                | WindowInsets.Type.navigationBars()
                | WindowInsets.Type.displayCutout());

        return ((long) insets.left & 0xffffL)
                | (((long) insets.top & 0xffffL) << 16)
                | (((long) insets.right & 0xffffL) << 32)
                | (((long) insets.bottom & 0xffffL) << 48);
    }

    /**
     * Called once from native startup to decide whether Android TV should offer
     * the mouse-heavy debugger UI.  Hotplug is intentionally ignored until the
     * next app start so we do not enumerate input devices every GUI frame.
     */
    public static boolean hasPointingMouse() {
        int[] ids = InputDevice.getDeviceIds();

        for (int id : ids) {
            InputDevice dev = InputDevice.getDevice(id);
            int sources;

            if (dev == null)
                continue;

            sources = dev.getSources();
            if ((sources & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE)
                return true;
            if (Build.VERSION.SDK_INT >= 26 &&
                    (sources & InputDevice.SOURCE_MOUSE_RELATIVE) ==
                    InputDevice.SOURCE_MOUSE_RELATIVE)
                return true;
        }

        return false;
    }

    /**
     * Called from native code (pc_get_storage_root_path in sdl_if.c) via JNI.
     * Returns the app-specific external directories, one per mounted external
     * volume: index 0 is primary emulated storage, index >= 1 is a removable
     * microSD card (the Galaxy A16 and friends).  These paths need NO runtime
     * permission on any API level, so they are always browsable and are the
     * only SD-card location that works on Android TV / Fire TV, where the
     * all-files-access settings screen frequently does not exist.
     *
     * SDL only wraps the singular getExternalFilesDir(), which can never see a
     * removable card - hence this plural variant.
     */
    public static String[] getStorageRoots() {
        Context ctx = getContext();
        ArrayList<String> roots = new ArrayList<>();

        if (ctx == null)
            return new String[0];

        for (File dir : ctx.getExternalFilesDirs(null)) {
            /* Entries are null for a volume that is currently unmounted. */
            if (dir == null)
                continue;
            /* mkdirs so a freshly inserted card has the directory to browse. */
            if (!dir.exists() && !dir.mkdirs())
                continue;
            roots.add(dir.getAbsolutePath());
        }

        return roots.toArray(new String[0]);
    }

    /**
     * True when the app can read AND write arbitrary paths outside its own
     * directories - i.e. a ROM folder the user already keeps at the root of the
     * SD card, which the save dialogs must also be able to write into.
     *
     * API 30+ requires MANAGE_EXTERNAL_STORAGE ("All files access"), the only
     * mechanism short of SAF that still grants raw path access to a removable
     * volume under scoped storage; it covers both read and write.  On API 23-29
     * the legacy runtime grants apply (the manifest sets
     * requestLegacyExternalStorage so API 29 keeps raw paths) - both are
     * checked, because READ alone would let the browser offer whole-volume
     * roots that every save would then fail on.  Below 23 the manifest
     * permissions are granted at install time.
     */
    public static boolean hasFullStorageAccess() {
        Context ctx = getContext();

        if (Build.VERSION.SDK_INT >= 30)
            return Environment.isExternalStorageManager();

        if (Build.VERSION.SDK_INT >= 23) {
            if (ctx == null)
                return false;
            return ctx.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
                        == PackageManager.PERMISSION_GRANTED
                && ctx.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                        == PackageManager.PERMISSION_GRANTED;
        }

        return true;
    }

    /**
     * Ask for the access described above.  Deliberately fire-and-forget: the
     * API 30+ path leaves the app for a Settings toggle, so there is no useful
     * synchronous result and no onActivityResult plumbing back to the SDL
     * thread.  Native code simply re-queries hasFullStorageAccess() the next
     * time the file browser opens.
     *
     * Returns false when the request could not even be shown - notably on
     * Android TV / Fire TV builds that ship no all-files-access screen - so the
     * caller can tell the user to use the SD card's app folder instead.
     */
    public static boolean requestFullStorageAccess() {
        final Activity activity = (Activity) getContext();

        if (activity == null || hasFullStorageAccess())
            return hasFullStorageAccess();

        if (Build.VERSION.SDK_INT >= 30) {
            /* Prefer the per-app screen; some builds only honour the global
             * list, and TV builds may have neither. */
            final Intent perApp = new Intent(
                    Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
                    Uri.parse("package:" + activity.getPackageName()));
            final Intent global = new Intent(
                    Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);

            if (perApp.resolveActivity(activity.getPackageManager()) == null
                    && global.resolveActivity(activity.getPackageManager()) == null)
                return false;

            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    try {
                        activity.startActivity(perApp);
                    } catch (Exception e) {
                        try {
                            activity.startActivity(global);
                        } catch (Exception ignored) {
                            /* Nothing else to try; caller already saw true, but
                             * the following access check will still report
                             * false, so the UI stays honest. */
                        }
                    }
                }
            });
            return true;
        }

        if (Build.VERSION.SDK_INT >= 23) {
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    /* Both, so browsing and saving are granted together; they
                     * share a permission group, so this is still one prompt. */
                    activity.requestPermissions(
                            new String[] { Manifest.permission.READ_EXTERNAL_STORAGE,
                                           Manifest.permission.WRITE_EXTERNAL_STORAGE },
                            KAT_STORAGE_PERMISSION_REQUEST);
                }
            });
            return true;
        }

        return true;
    }

    /** Arbitrary; SDLActivity ignores request codes it does not recognise. */
    private static final int KAT_STORAGE_PERMISSION_REQUEST = 5200;

    private static boolean detectTv(Context ctx) {
        if (ctx == null)
            return false;
        UiModeManager uiMode =
            (UiModeManager) ctx.getSystemService(Context.UI_MODE_SERVICE);
        return uiMode != null
            && uiMode.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
    }

    /**
     * SDL pins the activity to the orientation matching the window's aspect
     * when it is first created (e.g. USER_PORTRAIT if launched in portrait), so
     * the app "sticks" to its launch orientation and never follows the sensor.
     * On a phone, ignore those calls and keep free rotation (matches the
     * manifest's fullUser; respects the system auto-rotate lock).  On a TV
     * there is no sensor and the UI is a fixed 10-foot landscape layout, so
     * lock to landscape instead.
     */
    @Override
    public void setRequestedOrientation(int requestedOrientation) {
        if (isAndroidTv())
            super.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        else
            super.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_USER);
    }
}
