package de.ladkau.questshock;

import android.Manifest;
import android.app.AlertDialog;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.SurfaceHolder;
import androidx.core.app.ActivityCompat;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.libsdl.app.SDLActivity;
import org.libsdl.app.SDLSurface;

/**
 * Prepares /sdcard/questshock/ the way Shockolate's plain relative-path
 * file I/O ("res/data/...", confirmed via grep - none of it goes through
 * SDL_RWops, so Android's asset-manager fallback for SDL_RWFromFile
 * doesn't apply here) expects to find it - see android/engine-patches/ for
 * why this is done from Java instead of patching engine/ itself, which is
 * never modified.
 *
 * super.onCreate() (SDLActivity's) must always run first and unconditionally
 * - Android throws SuperNotCalledException otherwise, checked right after
 * onCreate() returns, regardless of what this subclass does afterwards.
 * SDLActivity's own onCreate() only loads libraries, sets up JNI and the
 * surface - the actual native SDL_main thread doesn't start until later, from
 * one of several lifecycle paths (onResume(), onWindowFocusChanged(), and
 * SDLSurface.surfaceChanged() - see GameSurface below for why that last one
 * needs its own fix) - so it's safe to do our own checks (and set
 * SDLActivity.mBrokenLibraries, which most - but not all - of those paths
 * gate on) afterwards.
 */
public class QuestShockActivity extends SDLActivity {
    private static final String TAG = "QuestShock";
    private static final int PERMISSION_REQUEST_STORAGE = 1;
    private static final String GAME_DIR = "/sdcard/questshock";

    // Loaded here (rather than waiting for SDLActivity's own
    // getLibraries()/loadLibraries(), which only runs from super.onCreate())
    // purely to get nativeChdir() below - Android's dynamic linker resolves
    // "main"'s own dependencies (SDL2, SDL2_mixer, fluidsynth) from the
    // APK's native library directory regardless of Java-side load order, sox
    // loading it early here is safe. SDLActivity loading "main" again later
    // is a harmless no-op (System.loadLibrary is idempotent per
    // ClassLoader).
    static {
        System.loadLibrary("main");
    }

    // See android/app/src/main/cpp/questshock_native.c - android.system.Os
    // has no public chdir() (confirmed against the actual API 34 stub jar).
    private static native void nativeChdir(String path);

    @Override
    protected String[] getLibraries() {
        return new String[] {
            "SDL2",
            "SDL2_mixer",
            "fluidsynth",
            "main"
        };
    }

    // SDLSurface.surfaceChanged() (org/libsdl/app/, vendored from SDL2's own
    // template) starts the native SDL thread directly - unlike onResume()/
    // onWindowFocusChanged(), it never checks SDLActivity.mBrokenLibraries
    // first. Since surfaceChanged() fires on essentially every launch
    // regardless of that flag, setting mBrokenLibraries alone (see onCreate()/
    // setUpGameDirAndContinue()) does not actually stop the engine from
    // starting. Route through a subclass that adds the missing check instead
    // of patching the vendored file directly.
    private static class GameSurface extends SDLSurface {
        GameSurface(Context context) {
            super(context);
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            if (SDLActivity.mBrokenLibraries) {
                return;
            }
            super.surfaceChanged(holder, format, width, height);
        }
    }

    @Override
    protected SDLSurface createSDLSurface(Context context) {
        return new GameSurface(context);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (SDLActivity.mBrokenLibraries) {
            // SDLActivity's own onCreate() already showed its "SDL Error"
            // dialog for this - nothing left for us to do.
            return;
        }
        // Provisionally block the native engine from starting - cleared only
        // once setUpGameDirAndContinue() confirms game data is present. Must
        // happen before requestPermissions() below: the storage-permission
        // dialog closing can fire onWindowFocusChanged(true) - which starts
        // the native SDLThread - before the async onRequestPermissionsResult()
        // callback (which is what actually calls setUpGameDirAndContinue())
        // gets a chance to run, so mBrokenLibraries has to already be true
        // going into that race, not set afterwards.
        SDLActivity.mBrokenLibraries = true;
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
                new String[] { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE },
                PERMISSION_REQUEST_STORAGE);
            return;
        }
        setUpGameDirAndContinue();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        if (requestCode == PERMISSION_REQUEST_STORAGE) {
            setUpGameDirAndContinue();
            return;
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    private void setUpGameDirAndContinue() {
        File gameDir = new File(GAME_DIR);
        if (!gameDir.exists() && !gameDir.mkdirs()) {
            Log.e(TAG, "Could not create " + GAME_DIR + " - res/data and res/sound will not be found");
        }

        // Extract our own bundled build artifacts (shaders, the soundfont,
        // and the "get assets" instructions) - never the proprietary game
        // data, which the user must supply themselves (see
        // res/GET_ASSETS_QUEST.txt). Only done once: if res/GET_ASSETS_QUEST.txt
        // is already there, assume a previous run already extracted everything.
        File marker = new File(gameDir, "GET_ASSETS_QUEST.txt");
        if (!marker.exists()) {
            copyAssetDir("shaders", new File(gameDir, "shaders"));
            copyAssetFile("res/soundfont.sf2", new File(gameDir, "res/soundfont.sf2"));
            copyAssetFile("GET_ASSETS_QUEST.txt", marker);
        }

        // Shockolate's engine/ is never patched to check this itself (see the
        // class doc above) - it just does plain fopen("res/data/...", ...)
        // and, on a fresh install with no game data copied in yet, that
        // fails deep inside startup (init_popups(), which doesn't NULL-check
        // the load) as a hard native crash instead of a message. Catch the
        // missing-data case here instead, before Shockolate's native
        // SDL_main ever starts.
        if (!isNonEmptyDir(new File(gameDir, "res/data")) || !isNonEmptyDir(new File(gameDir, "res/sound"))) {
            // mBrokenLibraries is already true (set in onCreate()) - leave it
            // that way. Every path that starts the native SDL_main thread
            // (onWindowFocusChanged(), resumeNativeThread(), etc.) already
            // checks this flag before doing anything native, so this
            // reliably prevents the engine from starting without having to
            // duplicate all of SDLActivity's own lifecycle guards ourselves.
            showMissingAssetsDialog();
            return;
        }

        // chdir() is process-wide, not per-thread - already in effect for
        // every thread (including the one that will run Shockolate's own
        // SDL_main) by the time the native thread actually starts. Done
        // before clearing mBrokenLibraries below so the engine can never
        // start pre-chdir.
        nativeChdir(GAME_DIR);

        // Assets confirmed present - safe to let the native engine start now.
        SDLActivity.mBrokenLibraries = false;
    }

    private static boolean isNonEmptyDir(File dir) {
        String[] entries = dir.list();
        return entries != null && entries.length > 0;
    }

    // Same single-button, non-cancelable pattern as SDLActivity's own
    // "broken libraries" dialog (org/libsdl/app/SDLActivity.java) - there's
    // no game to start without this data, so the only way forward is to
    // close, copy the assets, and relaunch. Message text mirrors
    // res/assets/GET_ASSETS_QUEST.txt (also extracted to GAME_DIR) so the
    // user isn't sent hunting for a second file just to read it the first
    // time - it points back there at the end in case they need it again.
    private void showMissingAssetsDialog() {
        new AlertDialog.Builder(this)
            .setTitle("Game data missing")
            .setMessage("This app does not include System Shock's game data - it's "
                + "copyrighted, proprietary content that can't be redistributed. To "
                + "play, you need to own a copy of System Shock: Enhanced Edition (e.g. "
                + "from gog.com), installed (on Windows, or via Wine/Proton on Linux).\n\n"
                + "Using SideQuest (or any MTP file browser) with your Quest connected, "
                + "copy its res/data/ and res/sound/ folders directly into:\n"
                + GAME_DIR + "/res/data/\n"
                + GAME_DIR + "/res/sound/\n"
                + "They already contain everything needed, in the right layout - no "
                + "merging or extraction required.\n\n"
                + "Then relaunch.\n\n"
                + "(These instructions are also in GET_ASSETS_QUEST.txt in the "
                + "questshock folder on this device, if you need to read them again.)")
            .setCancelable(false)
            .setPositiveButton("Exit", (dialog, which) -> finish())
            .create()
            .show();
    }

    private void copyAssetFile(String assetPath, File dest) {
        if (dest.exists()) {
            return;
        }
        File parent = dest.getParentFile();
        if (parent != null) {
            parent.mkdirs();
        }
        AssetManager assets = getAssets();
        try (InputStream in = assets.open(assetPath);
             OutputStream out = new FileOutputStream(dest)) {
            byte[] buf = new byte[64 * 1024];
            int n;
            while ((n = in.read(buf)) > 0) {
                out.write(buf, 0, n);
            }
        } catch (IOException e) {
            Log.e(TAG, "Could not extract asset " + assetPath + " to " + dest, e);
        }
    }

    private void copyAssetDir(String assetDir, File destDir) {
        AssetManager assets = getAssets();
        String[] entries;
        try {
            entries = assets.list(assetDir);
        } catch (IOException e) {
            Log.e(TAG, "Could not list asset dir " + assetDir, e);
            return;
        }
        if (entries == null || entries.length == 0) {
            return;
        }
        destDir.mkdirs();
        for (String entry : entries) {
            copyAssetFile(assetDir + "/" + entry, new File(destDir, entry));
        }
    }
}
