/*
 * Copyright 2013 Dolphin Emulator Project
 * Licensed under GPLv2+
 * Refer to the license.txt file included.
 */

package org.yuzu.yuzu_emu;

import android.app.Activity;
import android.app.Dialog;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.Bundle;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.Surface;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;

import com.google.android.material.dialog.MaterialAlertDialogBuilder;

import org.yuzu.yuzu_emu.activities.EmulationActivity;
import org.yuzu.yuzu_emu.utils.DocumentsTree;
import org.yuzu.yuzu_emu.utils.EmulationMenuSettings;
import org.yuzu.yuzu_emu.utils.FileUtil;
import org.yuzu.yuzu_emu.utils.Log;

import java.lang.ref.WeakReference;
import java.util.Objects;

import static android.Manifest.permission.CAMERA;
import static android.Manifest.permission.RECORD_AUDIO;

/**
 * Class which contains methods that interact
 * with the native side of the Citra code.
 */
public final class NativeLibrary {
    /**
     * Default controller id for each device
     */
    public static final int Player1Device = 0;
    public static final int Player2Device = 1;
    public static final int Player3Device = 2;
    public static final int Player4Device = 3;
    public static final int Player5Device = 4;
    public static final int Player6Device = 5;
    public static final int Player7Device = 6;
    public static final int Player8Device = 7;
    public static final int ConsoleDevice = 8;
    public static WeakReference<EmulationActivity> sEmulationActivity = new WeakReference<>(null);

    private static boolean alertResult = false;
    private static String alertPromptResult = "";
    private static int alertPromptButton = 0;
    private static final Object alertPromptLock = new Object();
    private static boolean alertPromptInProgress = false;
    private static String alertPromptCaption = "";
    private static int alertPromptButtonConfig = 0;
    private static EditText alertPromptEditText = null;

    static {
        try {
            System.loadLibrary("yuzu-android");
        } catch (UnsatisfiedLinkError ex) {
            Log.error("[NativeLibrary] " + ex.toString());
        }
    }

    private NativeLibrary() {
        // Disallows instantiation.
    }

    public static int openContentUri(String path, String openmode) {
        if (DocumentsTree.isNativePath(path)) {
            return YuzuApplication.documentsTree.openContentUri(path, openmode);
        }
        return FileUtil.openContentUri(YuzuApplication.getAppContext(), path, openmode);
    }

    public static long getSize(String path) {
        if (DocumentsTree.isNativePath(path)) {
            return YuzuApplication.documentsTree.getFileSize(path);
        }
        return FileUtil.getFileSize(YuzuApplication.getAppContext(), path);
    }

    /**
     * Handles button press events for a gamepad.
     *
     * @param Device The input descriptor of the gamepad.
     * @param Button Key code identifying which button was pressed.
     * @param Action Mask identifying which action is happening (button pressed down, or button released).
     * @return If we handled the button press.
     */
    public static native boolean onGamePadButtonEvent(int Device, int Button, int Action);

    /**
     * Handles joystick movement events.
     *
     * @param Device The device ID of the gamepad.
     * @param Axis   The axis ID
     * @param x_axis The value of the x-axis represented by the given ID.
     * @param y_axis The value of the y-axis represented by the given ID.
     */
    public static native boolean onGamePadJoystickEvent(int Device, int Axis, float x_axis, float y_axis);

    /**
     * Handles motion events.
     *
     * @param delta_timestamp         The finger id corresponding to this event
     * @param gyro_x,gyro_y,gyro_z    The value of the accelerometer sensor.
     * @param accel_x,accel_y,accel_z The value of the y-axis
     */

    public static native boolean onGamePadMotionEvent(int Device, long delta_timestamp, float gyro_x, float gyro_y,
                                                      float gyro_z, float accel_x, float accel_y, float accel_z);

    /**
     * Signals and load a nfc tag
     *
     * @param data         Byte array containing all the data from a nfc tag
     */
    public static native boolean onReadNfcTag(byte[] data);

    /**
     * Removes current loaded nfc tag
     */
    public static native boolean onRemoveNfcTag();

    /**
     * Handles touch press events.
     *
     * @param finger_id The finger id corresponding to this event
     * @param x_axis    The value of the x-axis.
     * @param y_axis    The value of the y-axis.
     */
    public static native void onTouchPressed(int finger_id, float x_axis, float y_axis);

    /**
     * Handles touch movement.
     *
     * @param x_axis The value of the instantaneous x-axis.
     * @param y_axis The value of the instantaneous y-axis.
     */
    public static native void onTouchMoved(int finger_id, float x_axis, float y_axis);

    /**
     * Handles touch release events.
     *
     * @param finger_id The finger id corresponding to this event
     */
    public static native void onTouchReleased(int finger_id);

    public static native void ReloadSettings();

    public static native String GetUserSetting(String gameID, String Section, String Key);

    public static native void SetUserSetting(String gameID, String Section, String Key, String Value);

    public static native void InitGameIni(String gameID);

    /**
     * Gets the embedded icon within the given ROM.
     *
     * @param filename the file path to the ROM.
     * @return a byte array containing the JPEG data for the icon.
     */
    public static native byte[] GetIcon(String filename);

    /**
     * Gets the embedded title of the given ISO/ROM.
     *
     * @param filename The file path to the ISO/ROM.
     * @return the embedded title of the ISO/ROM.
     */
    public static native String GetTitle(String filename);

    public static native String GetDescription(String filename);

    public static native String GetGameId(String filename);

    public static native String GetRegions(String filename);

    public static native String GetCompany(String filename);

    public static native String GetGitRevision();

    public static native void SetAppDirectory(String directory);

    public static native void InitializeGpuDriver(String hookLibDir, String customDriverDir, String customDriverName, String fileRedirectDir);

    public static native boolean ReloadKeys();

    public static native void InitializeEmulation();

    public static native int DefaultCPUCore();

    /**
     * Begins emulation.
     */
    public static native void Run(String path);

    /**
     * Begins emulation from the specified savestate.
     */
    public static native void Run(String path, String savestatePath, boolean deleteSavestate);

    // Surface Handling
    public static native void SurfaceChanged(Surface surf);

    public static native void SurfaceDestroyed();

    public static native void DoFrame();

    /**
     * Unpauses emulation from a paused state.
     */
    public static native void UnPauseEmulation();

    /**
     * Pauses emulation.
     */
    public static native void PauseEmulation();

    /**
     * Stops emulation.
     */
    public static native void StopEmulation();

    /**
     * Resets the in-memory ROM metadata cache.
     */
    public static native void ResetRomMetadata();

    /**
     * Returns true if emulation is running (or is paused).
     */
    public static native boolean IsRunning();

    /**
     * Returns the performance stats for the current game
     **/
    public static native double[] GetPerfStats();

    /**
     * Notifies the core emulation that the orientation has changed.
     */
    public static native void NotifyOrientationChange(int layout_option, int rotation);

    public enum CoreError {
        ErrorSystemFiles,
        ErrorSavestate,
        ErrorUnknown,
    }

    private static boolean coreErrorAlertResult = false;
    private static final Object coreErrorAlertLock = new Object();

    public static class CoreErrorDialogFragment extends DialogFragment {
        static CoreErrorDialogFragment newInstance(String title, String message) {
            CoreErrorDialogFragment frag = new CoreErrorDialogFragment();
            Bundle args = new Bundle();
            args.putString("title", title);
            args.putString("message", message);
            frag.setArguments(args);
            return frag;
        }

        @NonNull
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            final Activity emulationActivity = Objects.requireNonNull(getActivity());

            final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title"));
            final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message"));

            return new MaterialAlertDialogBuilder(emulationActivity)
                    .setTitle(title)
                    .setMessage(message)
                    .setPositiveButton(R.string.continue_button, (dialog, which) -> {
                        coreErrorAlertResult = true;
                        synchronized (coreErrorAlertLock) {
                            coreErrorAlertLock.notify();
                        }
                    })
                    .setNegativeButton(R.string.abort_button, (dialog, which) -> {
                        coreErrorAlertResult = false;
                        synchronized (coreErrorAlertLock) {
                            coreErrorAlertLock.notify();
                        }
                    }).setOnDismissListener(dialog -> {
                        coreErrorAlertResult = true;
                        synchronized (coreErrorAlertLock) {
                            coreErrorAlertLock.notify();
                        }
                    }).create();
        }
    }

    private static void OnCoreErrorImpl(String title, String message) {
        final EmulationActivity emulationActivity = sEmulationActivity.get();
        if (emulationActivity == null) {
            Log.error("[NativeLibrary] EmulationActivity not present");
            return;
        }

        CoreErrorDialogFragment fragment = CoreErrorDialogFragment.newInstance(title, message);
        fragment.show(emulationActivity.getSupportFragmentManager(), "coreError");
    }

    /**
     * Handles a core error.
     *
     * @return true: continue; false: abort
     */
    public static boolean OnCoreError(CoreError error, String details) {
        final EmulationActivity emulationActivity = sEmulationActivity.get();
        if (emulationActivity == null) {
            Log.error("[NativeLibrary] EmulationActivity not present");
            return false;
        }

        String title, message;
        switch (error) {
            case ErrorSystemFiles: {
                title = emulationActivity.getString(R.string.system_archive_not_found);
                message = emulationActivity.getString(R.string.system_archive_not_found_message, details.isEmpty() ? emulationActivity.getString(R.string.system_archive_general) : details);
                break;
            }
            case ErrorSavestate: {
                title = emulationActivity.getString(R.string.save_load_error);
                message = details;
                break;
            }
            case ErrorUnknown: {
                title = emulationActivity.getString(R.string.fatal_error);
                message = emulationActivity.getString(R.string.fatal_error_message);
                break;
            }
            default: {
                return true;
            }
        }

        // Show the AlertDialog on the main thread.
        emulationActivity.runOnUiThread(() -> OnCoreErrorImpl(title, message));

        // Wait for the lock to notify that it is complete.
        synchronized (coreErrorAlertLock) {
            try {
                coreErrorAlertLock.wait();
            } catch (Exception ignored) {
            }
        }

        return coreErrorAlertResult;
    }

    public static boolean isPortraitMode() {
        return YuzuApplication.getAppContext().getResources().getConfiguration().orientation ==
                Configuration.ORIENTATION_PORTRAIT;
    }

    public static int landscapeScreenLayout() {
        return EmulationMenuSettings.getLandscapeScreenLayout();
    }

    public static boolean displayAlertMsg(final String caption, final String text,
                                          final boolean yesNo) {
        Log.error("[NativeLibrary] Alert: " + text);
        final EmulationActivity emulationActivity = sEmulationActivity.get();
        boolean result = false;
        if (emulationActivity == null) {
            Log.warning("[NativeLibrary] EmulationActivity is null, can't do panic alert.");
        } else {
            // Create object used for waiting.
            final Object lock = new Object();
            MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
                    .setTitle(caption)
                    .setMessage(text);

            // If not yes/no dialog just have one button that dismisses modal,
            // otherwise have a yes and no button that sets alertResult accordingly.
            if (!yesNo) {
                builder
                        .setCancelable(false)
                        .setPositiveButton(android.R.string.ok, (dialog, whichButton) ->
                        {
                            dialog.dismiss();
                            synchronized (lock) {
                                lock.notify();
                            }
                        });
            } else {
                alertResult = false;

                builder
                        .setPositiveButton(android.R.string.yes, (dialog, whichButton) ->
                        {
                            alertResult = true;
                            dialog.dismiss();
                            synchronized (lock) {
                                lock.notify();
                            }
                        })
                        .setNegativeButton(android.R.string.no, (dialog, whichButton) ->
                        {
                            alertResult = false;
                            dialog.dismiss();
                            synchronized (lock) {
                                lock.notify();
                            }
                        });
            }

            // Show the AlertDialog on the main thread.
            emulationActivity.runOnUiThread(builder::show);

            // Wait for the lock to notify that it is complete.
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (Exception e) {
                }
            }

            if (yesNo)
                result = alertResult;
        }
        return result;
    }

    public static void retryDisplayAlertPrompt() {
        if (!alertPromptInProgress) {
            return;
        }
        displayAlertPromptImpl(alertPromptCaption, alertPromptEditText.getText().toString(), alertPromptButtonConfig).show();
    }

    public static String displayAlertPrompt(String caption, String text, int buttonConfig) {
        alertPromptCaption = caption;
        alertPromptButtonConfig = buttonConfig;
        alertPromptInProgress = true;

        // Show the AlertDialog on the main thread
        sEmulationActivity.get().runOnUiThread(() -> displayAlertPromptImpl(alertPromptCaption, text, alertPromptButtonConfig).show());

        // Wait for the lock to notify that it is complete
        synchronized (alertPromptLock) {
            try {
                alertPromptLock.wait();
            } catch (Exception e) {
            }
        }
        alertPromptInProgress = false;

        return alertPromptResult;
    }

    public static MaterialAlertDialogBuilder displayAlertPromptImpl(String caption, String text, int buttonConfig) {
        final EmulationActivity emulationActivity = sEmulationActivity.get();
        alertPromptResult = "";
        alertPromptButton = 0;

        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        params.leftMargin = params.rightMargin = YuzuApplication.getAppContext().getResources().getDimensionPixelSize(R.dimen.dialog_margin);

        // Set up the input
        alertPromptEditText = new EditText(YuzuApplication.getAppContext());
        alertPromptEditText.setText(text);
        alertPromptEditText.setSingleLine();
        alertPromptEditText.setLayoutParams(params);

        FrameLayout container = new FrameLayout(emulationActivity);
        container.addView(alertPromptEditText);

        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
                .setTitle(caption)
                .setView(container)
                .setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
                {
                    alertPromptButton = buttonConfig;
                    alertPromptResult = alertPromptEditText.getText().toString();
                    synchronized (alertPromptLock) {
                        alertPromptLock.notifyAll();
                    }
                })
                .setOnDismissListener(dialogInterface ->
                {
                    alertPromptResult = "";
                    synchronized (alertPromptLock) {
                        alertPromptLock.notifyAll();
                    }
                });

        if (buttonConfig > 0) {
            builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) ->
            {
                alertPromptResult = "";
                synchronized (alertPromptLock) {
                    alertPromptLock.notifyAll();
                }
            });
        }

        return builder;
    }

    public static int alertPromptButton() {
        return alertPromptButton;
    }

    public static void exitEmulationActivity(int resultCode) {
        final int Success = 0;
        final int ErrorNotInitialized = 1;
        final int ErrorGetLoader = 2;
        final int ErrorSystemFiles = 3;
        final int ErrorSharedFont = 4;
        final int ErrorVideoCore = 5;
        final int ErrorUnknown = 6;
        final int ErrorLoader = 7;

        int captionId;
        int descriptionId;
        switch (resultCode) {
            case ErrorVideoCore:
                captionId = R.string.loader_error_video_core;
                descriptionId = R.string.loader_error_video_core_description;
                break;
            default:
                captionId = R.string.loader_error_encrypted;
                descriptionId = R.string.loader_error_encrypted_roms_description;
                if (!ReloadKeys()) {
                    descriptionId = R.string.loader_error_encrypted_keys_description;
                }
                break;
        }

        final EmulationActivity emulationActivity = sEmulationActivity.get();
        if (emulationActivity == null) {
            Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.");
            return;
        }

        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
                .setTitle(captionId)
                .setMessage(Html.fromHtml(emulationActivity.getString(descriptionId), Html.FROM_HTML_MODE_LEGACY))
                .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> emulationActivity.finish())
                .setOnDismissListener(dialogInterface -> emulationActivity.finish());
        emulationActivity.runOnUiThread(() -> {
            AlertDialog alert = builder.create();
            alert.show();
            ((TextView) alert.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
        });
    }

    public static void setEmulationActivity(EmulationActivity emulationActivity) {
        Log.verbose("[NativeLibrary] Registering EmulationActivity.");
        sEmulationActivity = new WeakReference<>(emulationActivity);
    }

    public static void clearEmulationActivity() {
        Log.verbose("[NativeLibrary] Unregistering EmulationActivity.");

        sEmulationActivity.clear();
    }

    private static final Object cameraPermissionLock = new Object();
    private static boolean cameraPermissionGranted = false;
    public static final int REQUEST_CODE_NATIVE_CAMERA = 800;

    public static boolean RequestCameraPermission() {
        final EmulationActivity emulationActivity = sEmulationActivity.get();
        if (emulationActivity == null) {
            Log.error("[NativeLibrary] EmulationActivity not present");
            return false;
        }
        if (ContextCompat.checkSelfPermission(emulationActivity, CAMERA) == PackageManager.PERMISSION_GRANTED) {
            // Permission already granted
            return true;
        }
        emulationActivity.requestPermissions(new String[]{CAMERA}, REQUEST_CODE_NATIVE_CAMERA);

        // Wait until result is returned
        synchronized (cameraPermissionLock) {
            try {
                cameraPermissionLock.wait();
            } catch (InterruptedException ignored) {
            }
        }
        return cameraPermissionGranted;
    }

    public static void CameraPermissionResult(boolean granted) {
        cameraPermissionGranted = granted;
        synchronized (cameraPermissionLock) {
            cameraPermissionLock.notify();
        }
    }

    private static final Object micPermissionLock = new Object();
    private static boolean micPermissionGranted = false;
    public static final int REQUEST_CODE_NATIVE_MIC = 900;

    public static boolean RequestMicPermission() {
        final EmulationActivity emulationActivity = sEmulationActivity.get();
        if (emulationActivity == null) {
            Log.error("[NativeLibrary] EmulationActivity not present");
            return false;
        }
        if (ContextCompat.checkSelfPermission(emulationActivity, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
            // Permission already granted
            return true;
        }
        emulationActivity.requestPermissions(new String[]{RECORD_AUDIO}, REQUEST_CODE_NATIVE_MIC);

        // Wait until result is returned
        synchronized (micPermissionLock) {
            try {
                micPermissionLock.wait();
            } catch (InterruptedException ignored) {
            }
        }
        return micPermissionGranted;
    }

    public static void MicPermissionResult(boolean granted) {
        micPermissionGranted = granted;
        synchronized (micPermissionLock) {
            micPermissionLock.notify();
        }
    }

    /**
     * Logs the Citra version, Android version and, CPU.
     */
    public static native void LogDeviceInfo();

    /**
     * Submits inline keyboard text. Called on input for buttons that result text.
     * @param text Text to submit to the inline software keyboard implementation.
     */
    public static native void SubmitInlineKeyboardText(String text);

    /**
     * Submits inline keyboard input. Used to indicate keys pressed that are not text.
     * @param key_code Android Key Code associated with the keyboard input.
     */
    public static native void SubmitInlineKeyboardInput(int key_code);

    /**
     * Button type for use in onTouchEvent
     */
    public static final class ButtonType {
        public static final int BUTTON_A = 0;
        public static final int BUTTON_B = 1;
        public static final int BUTTON_X = 2;
        public static final int BUTTON_Y = 3;
        public static final int STICK_L = 4;
        public static final int STICK_R = 5;
        public static final int TRIGGER_L = 6;
        public static final int TRIGGER_R = 7;
        public static final int TRIGGER_ZL = 8;
        public static final int TRIGGER_ZR = 9;
        public static final int BUTTON_PLUS = 10;
        public static final int BUTTON_MINUS = 11;
        public static final int DPAD_LEFT = 12;
        public static final int DPAD_UP = 13;
        public static final int DPAD_RIGHT = 14;
        public static final int DPAD_DOWN = 15;
        public static final int BUTTON_SL = 16;
        public static final int BUTTON_SR = 17;
        public static final int BUTTON_HOME = 18;
        public static final int BUTTON_CAPTURE = 19;
    }

    /**
     * Stick type for use in onTouchEvent
     */
    public static final class StickType {
        public static final int STICK_L = 0;
        public static final int STICK_R = 1;
    }

    /**
     * Button states
     */
    public static final class ButtonState {
        public static final int RELEASED = 0;
        public static final int PRESSED = 1;
    }
}
