/*
 * Hello Minecraft! Launcher
 * Copyright (C) 2020  huangyuhui <huanghongxun2008@126.com> and contributors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package org.jackhuang.hmcl;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.SambaException;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.CrashReporter;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;

import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;

public final class Launcher extends Application {
    public static final CookieManager COOKIE_MANAGER = new CookieManager();

    @Override
    public void start(Stage primaryStage) {
        Thread.currentThread().setUncaughtExceptionHandler(CRASH_REPORTER);

        CookieHandler.setDefault(COOKIE_MANAGER);

        LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version"));
        try {
            Object pipeline = Class.forName("com.sun.prism.GraphicsPipeline").getMethod("getPipeline").invoke(null);
            LOG.info("Prism pipeline: " + (pipeline == null ? "null" : pipeline.getClass().getName()));
        } catch (Throwable e) {
            LOG.warning("Failed to get prism pipeline", e);
        }

        try {
            try {
                ConfigHolder.init();
            } catch (SambaException ignored) {
                Main.showWarningAndContinue(i18n("fatal.samba"));
            } catch (IOException e) {
                LOG.error("Failed to load config", e);
                checkConfigInTempDir();
                checkConfigOwner();
                Main.showErrorAndExit(i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
            }

            // https://lapcatsoftware.com/articles/app-translocation.html
            if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX
                    && ConfigHolder.isNewlyCreated()
                    && System.getProperty("user.dir").startsWith("/private/var/folders/")) {
                if (showAlert(AlertType.WARNING, i18n("fatal.mac_app_translocation"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
                    return;
            } else {
                checkConfigInTempDir();
            }

            if (ConfigHolder.isOwnerChanged()) {
                if (showAlert(AlertType.WARNING, i18n("fatal.config_change_owner_root"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
                    return;
            }

            if (Metadata.HMCL_CURRENT_DIRECTORY.toString().indexOf('=') >= 0) {
                Main.showWarningAndContinue(i18n("fatal.illegal_char"));
            }

            // runLater to ensure ConfigHolder.init() finished initialization
            Platform.runLater(() -> {
                // When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
                // Stage.show() cannot work again because JavaFX Toolkit have already shut down.
                Platform.setImplicitExit(false);
                Controllers.initialize(primaryStage);

                UpdateChecker.init();

                primaryStage.show();
            });
        } catch (Throwable e) {
            CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
        }
    }

    private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) {
        return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
    }

    private static boolean isConfigInTempDir() {
        String configPath = ConfigHolder.configLocation().toString();

        String tmpdir = System.getProperty("java.io.tmpdir");
        if (StringUtils.isNotBlank(tmpdir) && configPath.startsWith(tmpdir))
            return true;

        String[] tempFolderNames = {"Temp", "Cache", "Caches"};
        for (String name : tempFolderNames) {
            if (configPath.contains(File.separator + name + File.separator))
                return true;
        }

        if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
            return configPath.contains("\\Temporary Internet Files\\")
                    || configPath.contains("\\INetCache\\")
                    || configPath.contains("\\$Recycle.Bin\\")
                    || configPath.contains("\\recycler\\");
        } else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
            return configPath.startsWith("/tmp/")
                    || configPath.startsWith("/var/tmp/")
                    || configPath.startsWith("/var/cache/")
                    || configPath.startsWith("/dev/shm/")
                    || configPath.contains("/Trash/");
        } else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
            return configPath.startsWith("/var/folders/")
                    || configPath.startsWith("/private/var/folders/")
                    || configPath.startsWith("/tmp/")
                    || configPath.startsWith("/private/tmp/")
                    || configPath.startsWith("/var/tmp/")
                    || configPath.startsWith("/private/var/tmp/")
                    || configPath.contains("/.Trash/");
        } else {
            return false;
        }
    }

    private static void checkConfigInTempDir() {
        if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
                && showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
            Main.exit(0);
        }
    }

    private static void checkConfigOwner() {
        if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
            return;

        String userName = System.getProperty("user.name");
        String owner;
        try {
            owner = Files.getOwner(ConfigHolder.configLocation()).getName();
        } catch (IOException ioe) {
            LOG.warning("Failed to get file owner", ioe);
            return;
        }

        if (Files.isWritable(ConfigHolder.configLocation()) || userName.equals("root") || userName.equals(owner))
            return;

        ArrayList<String> files = new ArrayList<>();
        files.add(ConfigHolder.configLocation().toString());
        if (Files.exists(Metadata.HMCL_GLOBAL_DIRECTORY))
            files.add(Metadata.HMCL_GLOBAL_DIRECTORY.toString());
        if (Files.exists(Metadata.HMCL_CURRENT_DIRECTORY))
            files.add(Metadata.HMCL_CURRENT_DIRECTORY.toString());

        Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
        if (Files.exists(mcDir))
            files.add(mcDir.toString());

        String command = new CommandBuilder().add("sudo", "chown", "-R", userName).addAll(files).toString();
        ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));

        if (showAlert(AlertType.ERROR,
                i18n("fatal.config_loading_failure.unix", owner, command),
                copyAndExit, ButtonType.CLOSE) == copyAndExit) {
            Clipboard.getSystemClipboard()
                    .setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
        }
        Main.exit(1);
    }

    @Override
    public void stop() throws Exception {
        Controllers.onApplicationStop();
        LOG.shutdown();
    }

    public static void main(String[] args) {
        if (UpdateHandler.processArguments(args)) {
            LOG.shutdown();
            return;
        }

        Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
        AsyncTaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));

        try {
            LOG.info("*** " + Metadata.TITLE + " ***");
            LOG.info("Operating System: " + (OperatingSystem.OS_RELEASE_PRETTY_NAME == null
                    ? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION
                    : OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + ')'));
            if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
                LOG.info("Processor Identifier: " + System.getenv("PROCESSOR_IDENTIFIER"));
            }
            LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName());
            LOG.info("Native Encoding: " + OperatingSystem.NATIVE_CHARSET);
            LOG.info("JNU Encoding: " + System.getProperty("sun.jnu.encoding"));
            if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
                LOG.info("Code Page: " + OperatingSystem.CODE_PAGE);
            }
            LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH.getDisplayName());
            LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
            LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
            LOG.info("Java Home: " + System.getProperty("java.home"));
            LOG.info("Current Directory: " + Metadata.CURRENT_DIRECTORY);
            LOG.info("HMCL Global Directory: " + Metadata.HMCL_GLOBAL_DIRECTORY);
            LOG.info("HMCL Current Directory: " + Metadata.HMCL_CURRENT_DIRECTORY);
            LOG.info("HMCL Jar Path: " + Lang.requireNonNullElse(JarUtils.thisJarPath(), "Not Found"));
            LOG.info("HMCL Log File: " + Lang.requireNonNullElse(LOG.getLogFile(), "In Memory"));
            LOG.info("JVM Max Memory: " + MEGABYTES.formatBytes(Runtime.getRuntime().maxMemory()));
            try {
                for (MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) {
                    if ("Metaspace".equals(bean.getName())) {
                        long bytes = bean.getUsage().getUsed();
                        LOG.info("Metaspace: " + MEGABYTES.formatBytes(bytes));
                        break;
                    }
                }
            } catch (NoClassDefFoundError ignored) {
            }
            LOG.info("Native Backend: " + (NativeUtils.USE_JNA ? "JNA" : "None"));
            if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
                LOG.info("XDG Session Type: " + System.getenv("XDG_SESSION_TYPE"));
                LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));
            }

            Lang.thread(SystemInfo::initialize, "Detection System Information", true);

            launch(Launcher.class, args);
        } catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
            CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
        }
    }

    public static void stopApplication() {
        LOG.info("Stopping application.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));

        runInFX(() -> {
            if (Controllers.getStage() == null)
                return;
            Controllers.getStage().close();
            Schedulers.shutdown();
            Controllers.shutdown();
            Platform.exit();
        });
    }

    public static void stopWithoutPlatform() {
        LOG.info("Stopping application without JavaFX Toolkit.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));

        runInFX(() -> {
            if (Controllers.getStage() == null)
                return;
            Controllers.getStage().close();
            Schedulers.shutdown();
            Controllers.shutdown();
            Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
        });
    }

    public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
}
