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

package org.citra.citra_emu.utils;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Environment;
import android.preference.PreferenceManager;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicBoolean;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.NativeLibrary;

/**
 * A service that spawns its own thread in order to copy several binary and shader files
 * from the Citra APK to the external file system.
 */
public final class DirectoryInitialization {
    public static final String BROADCAST_ACTION = "org.citra.citra_emu.BROADCAST";

    public static final String EXTRA_STATE = "directoryState";
    private static volatile DirectoryInitializationState directoryState = null;
    private static String userPath;
    private static AtomicBoolean isCitraDirectoryInitializationRunning = new AtomicBoolean(false);

    public static void start(Context context) {
        // Can take a few seconds to run, so don't block UI thread.
        //noinspection TrivialFunctionalExpressionUsage
        ((Runnable) () -> init(context)).run();
    }

    private static void init(Context context) {
        if (!isCitraDirectoryInitializationRunning.compareAndSet(false, true))
            return;

        if (directoryState != DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) {
            if (PermissionsHandler.hasWriteAccess(context)) {
                if (setCitraUserDirectory()) {
                    initializeInternalStorage(context);
                    CitraApplication.documentsTree.setRoot(Uri.parse(userPath));
                    NativeLibrary.CreateLogFile();
                    NativeLibrary.LogUserDirectory(userPath);
                    NativeLibrary.CreateConfigFile();
                    directoryState = DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED;
                } else {
                    directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE;
                }
            } else {
                directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED;
            }
        }

        isCitraDirectoryInitializationRunning.set(false);
        sendBroadcastState(directoryState, context);
    }

    private static void deleteDirectoryRecursively(File file) {
        if (file.isDirectory()) {
            for (File child : file.listFiles())
                deleteDirectoryRecursively(child);
        }
        file.delete();
    }

    public static boolean areCitraDirectoriesReady() {
        return directoryState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED;
    }

    public static void resetCitraDirectoryState() {
        directoryState = null;
        isCitraDirectoryInitializationRunning.compareAndSet(true, false);
    }

    public static String getUserDirectory() {
        if (directoryState == null) {
            throw new IllegalStateException("DirectoryInitialization has to run at least once!");
        } else if (isCitraDirectoryInitializationRunning.get()) {
            throw new IllegalStateException(
                    "DirectoryInitialization has to finish running first!");
        }
        return userPath;
    }

    private static native void SetSysDirectory(String path);

    private static boolean setCitraUserDirectory() {
        Uri dataPath = PermissionsHandler.getCitraDirectory();
        if (dataPath != null) {
            userPath = dataPath.toString();
            Log.debug("[DirectoryInitialization] User Dir: " + userPath);
            return true;
        }

        return false;
    }

    private static void initializeInternalStorage(Context context) {
        File sysDirectory = new File(context.getFilesDir(), "Sys");

        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        String revision = NativeLibrary.GetGitRevision();
        if (!preferences.getString("sysDirectoryVersion", "").equals(revision)) {
            // There is no extracted Sys directory, or there is a Sys directory from another
            // version of Citra that might contain outdated files. Let's (re-)extract Sys.
            deleteDirectoryRecursively(sysDirectory);
            copyAssetFolder("Sys", sysDirectory, true, context);

            SharedPreferences.Editor editor = preferences.edit();
            editor.putString("sysDirectoryVersion", revision);
            editor.apply();
        }

        // Let the native code know where the Sys directory is.
        SetSysDirectory(sysDirectory.getPath());
    }

    private static void sendBroadcastState(DirectoryInitializationState state, Context context) {
        Intent localIntent =
                new Intent(BROADCAST_ACTION)
                        .putExtra(EXTRA_STATE, state);
        LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
    }

    private static void copyAsset(String asset, File output, Boolean overwrite, Context context) {
        Log.verbose("[DirectoryInitialization] Copying File " + asset + " to " + output);

        try {
            if (!output.exists() || overwrite) {
                InputStream in = context.getAssets().open(asset);
                OutputStream out = new FileOutputStream(output);
                copyFile(in, out);
                in.close();
                out.close();
            }
        } catch (IOException e) {
            Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset +
                    e.getMessage());
        }
    }

    private static void copyAssetFolder(String assetFolder, File outputFolder, Boolean overwrite,
                                        Context context) {
        Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " +
                outputFolder);

        try {
            boolean createdFolder = false;
            for (String file : context.getAssets().list(assetFolder)) {
                if (!createdFolder) {
                    outputFolder.mkdir();
                    createdFolder = true;
                }
                copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file),
                        overwrite, context);
                copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), overwrite,
                        context);
            }
        } catch (IOException e) {
            Log.error("[DirectoryInitialization] Failed to copy asset folder: " + assetFolder +
                    e.getMessage());
        }
    }

    private static void copyFile(InputStream in, OutputStream out) throws IOException {
        byte[] buffer = new byte[1024];
        int read;

        while ((read = in.read(buffer)) != -1) {
            out.write(buffer, 0, read);
        }
    }

    public enum DirectoryInitializationState {
        CITRA_DIRECTORIES_INITIALIZED,
        EXTERNAL_STORAGE_PERMISSION_NEEDED,
        CANT_FIND_EXTERNAL_STORAGE
    }
}
