package org.jabref.gui;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.swing.undo.UndoManager;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.collections.ListChangeListener;
import javafx.concurrent.Task;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region;
import javafx.scene.text.Font;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;

import org.jabref.gui.clipboard.ClipBoardManager;
import org.jabref.gui.frame.JabRefFrame;
import org.jabref.gui.help.VersionWorker;
import org.jabref.gui.icon.IconTheme;
import org.jabref.gui.keyboard.KeyBindingRepository;
import org.jabref.gui.keyboard.SelectableTextFlowKeyBindings;
import org.jabref.gui.keyboard.TextInputKeyBindings;
import org.jabref.gui.keyboard.WalkthroughKeyBindings;
import org.jabref.gui.openoffice.OOBibBaseConnect;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.remote.CLIMessageHandler;
import org.jabref.gui.theme.ThemeManager;
import org.jabref.gui.undo.CountingUndoManager;
import org.jabref.gui.util.DefaultFileUpdateMonitor;
import org.jabref.gui.util.DirectoryMonitor;
import org.jabref.gui.util.UiTaskExecutor;
import org.jabref.http.manager.HttpServerManager;
import org.jabref.languageserver.controller.LanguageServerController;
import org.jabref.logic.UiCommand;
import org.jabref.logic.ai.AiService;
import org.jabref.logic.citation.SearchCitationsRelationsService;
import org.jabref.logic.git.util.GitHandlerRegistry;
import org.jabref.logic.journals.JournalAbbreviationLoader;
import org.jabref.logic.journals.JournalAbbreviationRepository;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.net.ProxyRegisterer;
import org.jabref.logic.os.OS;
import org.jabref.logic.protectedterms.ProtectedTermsLoader;
import org.jabref.logic.remote.RemotePreferences;
import org.jabref.logic.remote.server.RemoteListenerServerManager;
import org.jabref.logic.search.sqlbased.IndexManager;
import org.jabref.logic.search.sqlbased.PostgresServer;
import org.jabref.logic.util.BuildInfo;
import org.jabref.logic.util.FallbackExceptionHandler;
import org.jabref.logic.util.HeadlessExecutorService;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.logic.util.strings.StringUtil;
import org.jabref.model.entry.BibEntryTypesManager;
import org.jabref.model.util.FileUpdateMonitor;

import com.airhacks.afterburner.injection.Injector;
import com.dlsc.gemsfx.PowerPane;
import com.dlsc.gemsfx.infocenter.InfoCenterPane;
import com.dlsc.gemsfx.infocenter.InfoCenterViewPos;
import com.tobiasdiez.easybind.EasyBind;
import io.github.kusoroadeolu.veneer.BibTeXSyntaxHighlighter;
import kong.unirest.core.Unirest;
import org.controlsfx.control.decoration.Decorator;
import org.controlsfx.control.decoration.GraphicDecoration;
import org.controlsfx.dialog.ExceptionDialog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/// Represents the outer stage and the scene of the JabRef window.
public class JabRefGUI extends Application {

    private static final Logger LOGGER = LoggerFactory.getLogger(JabRefGUI.class);

    private static List<UiCommand> uiCommands;
    private static GuiPreferences preferences;

    // AI Service handles chat messages etc. Therefore, it is tightly coupled to the GUI.
    private static AiService aiService;
    // CitationsAndRelationsSearchService is here configured for a local machine and so to the GUI.
    private static SearchCitationsRelationsService citationsAndRelationsSearchService;

    private static FileUpdateMonitor fileUpdateMonitor;
    private static StateManager stateManager;
    private static ThemeManager themeManager;
    private static CountingUndoManager countingUndoManager;
    private static TaskExecutor taskExecutor;
    private static ClipBoardManager clipBoardManager;
    private static final String BIBTEX_EDITOR_FONT_RESOURCE = "fonts/JetBrainsMono-Regular.ttf";

    private static DialogService dialogService;
    private static JabRefFrame mainFrame;
    private static GitHandlerRegistry gitHandlerRegistry;
    private static JournalAbbreviationRepository journalAbbreviationRepository;
    private static BibTeXSyntaxHighlighter bibTeXSyntaxHighlighter;

    private static RemoteListenerServerManager remoteListenerServerManager;
    private static HttpServerManager httpServerManager;
    private static LanguageServerController languageServerController;

    private Stage mainStage;

    public static void setup(List<UiCommand> uiCommands,
                             GuiPreferences preferences) {
        JabRefGUI.uiCommands = uiCommands;
        JabRefGUI.preferences = preferences;
    }

    @Override
    public void start(Stage stage) {
        // Installed before the try block below (not after) so background threads started during
        // initialize() are also covered, not just ones started once the main window is up.
        FallbackExceptionHandler.installExceptionHandler((exception, thread) -> UiTaskExecutor.runInJavaFXThread(() ->
                showCriticalErrorDialog(Localization.lang("Uncaught exception occurred in %0", thread.toString()), exception)));

        try {
            this.mainStage = stage;
            Injector.setModelOrService(Stage.class, mainStage);

            initialize();

            JabRefGUI.mainFrame = new JabRefFrame(
                    mainStage,
                    dialogService,
                    fileUpdateMonitor,
                    preferences,
                    aiService,
                    stateManager,
                    countingUndoManager,
                    Injector.instantiateModelOrService(BibEntryTypesManager.class),
                    clipBoardManager,
                    taskExecutor,
                    gitHandlerRegistry,
                    journalAbbreviationRepository);

            openWindow();

            startBackgroundTasks();
            setupHttpServerEnabledListener();

            if (!fileUpdateMonitor.isActive()) {
                dialogService.showErrorDialogAndWait(
                        Localization.lang("Unable to monitor file changes. Please close files " +
                                "and processes and restart. You may encounter errors if you continue " +
                                "with this session."));
            }

            BuildInfo buildInfo = Injector.instantiateModelOrService(BuildInfo.class);
            EasyBind.subscribe(preferences.getInternalPreferences().versionCheckEnabledProperty(), enabled -> {
                if (enabled) {
                    new VersionWorker(buildInfo.version,
                            dialogService,
                            taskExecutor,
                            preferences)
                            .checkForNewVersionDelayed();
                }
            });

            setupProxy();
        } catch (Throwable throwable) {
            LOGGER.error("Error during initialization", throwable);
            showCriticalErrorDialog(Localization.lang("Unhandled exception occurred."), throwable);
            throw throwable;
        }
    }

    /// [impl->req~ux.startup.critical-error-dialog~1]
    ///
    /// Used both for startup failures (from [#start(Stage)]'s catch block) and for uncaught
    /// exceptions on other threads ([FallbackExceptionHandler], installed before [#initialize()]
    /// so it also covers background threads started during startup). In both cases, [DialogService]
    /// or [Scene] may not exist yet: [DialogService] is only set up in [#initialize()], and `Scene`
    /// is only set in [#openWindow()]. Calling [DialogService] or `Dialog.initOwner` before then
    /// throws a NullPointerException, so this falls back to an ownerless [ExceptionDialog] instead.
    private void showCriticalErrorDialog(String header, Throwable throwable) {
        try {
            if (mainStage != null && mainStage.getScene() != null) {
                Injector.instantiateModelOrService(DialogService.class).showErrorDialogAndWait(header, throwable);
                return;
            }
            ExceptionDialog exceptionDialog = new ExceptionDialog(throwable);
            exceptionDialog.setHeaderText(header);
            exceptionDialog.showAndWait();
        } catch (Throwable dialogFailure) {
            LOGGER.error("Could not show critical error dialog", dialogFailure);
        }
    }

    public void initialize() {
        JabRefGUI.stateManager = new JabRefGuiStateManager();
        Injector.setModelOrService(StateManager.class, stateManager);

        // our Default task executor is the UITaskExecutor which can use the fx thread
        JabRefGUI.taskExecutor = new UiTaskExecutor();
        Injector.setModelOrService(TaskExecutor.class, taskExecutor);

        journalAbbreviationRepository = JournalAbbreviationLoader.loadRepositoryInBackground(preferences.getAbbreviationPreferences(), taskExecutor);

        DefaultFileUpdateMonitor fileUpdateMonitor = new DefaultFileUpdateMonitor();
        JabRefGUI.fileUpdateMonitor = fileUpdateMonitor;
        HeadlessExecutorService.INSTANCE.executeInterruptableTask(fileUpdateMonitor, "FileUpdateMonitor");
        Injector.setModelOrService(FileUpdateMonitor.class, fileUpdateMonitor);

        DirectoryMonitor directoryMonitor = new DirectoryMonitor();
        Injector.setModelOrService(DirectoryMonitor.class, directoryMonitor);

        gitHandlerRegistry = new GitHandlerRegistry(preferences.getGitPreferences());
        Injector.setModelOrService(GitHandlerRegistry.class, gitHandlerRegistry);

        BibEntryTypesManager entryTypesManager = preferences.getCustomEntryTypesRepository();
        Injector.setModelOrService(BibEntryTypesManager.class, entryTypesManager);
        Injector.setModelOrService(JournalAbbreviationRepository.class, journalAbbreviationRepository);
        Injector.setModelOrService(ProtectedTermsLoader.class, new ProtectedTermsLoader(preferences.getProtectedTermsPreferences()));

        IndexManager.clearOldSearchIndices();

        JabRefGUI.remoteListenerServerManager = new RemoteListenerServerManager();
        Injector.setModelOrService(RemoteListenerServerManager.class, JabRefGUI.remoteListenerServerManager);

        JabRefGUI.httpServerManager = new HttpServerManager();
        Injector.setModelOrService(HttpServerManager.class, JabRefGUI.httpServerManager);

        JabRefGUI.languageServerController = new LanguageServerController(preferences, journalAbbreviationRepository, entryTypesManager);
        Injector.setModelOrService(LanguageServerController.class, JabRefGUI.languageServerController);

        Injector.setModelOrService(KeyBindingRepository.class, preferences.getKeyBindingRepository());

        JabRefGUI.themeManager = new ThemeManager(
                preferences.getWorkspacePreferences(),
                fileUpdateMonitor
        );
        Injector.setModelOrService(ThemeManager.class, themeManager);

        JabRefGUI.countingUndoManager = new CountingUndoManager();
        Injector.setModelOrService(UndoManager.class, countingUndoManager);
        Injector.setModelOrService(CountingUndoManager.class, countingUndoManager);

        JabRefGUI.dialogService = new JabRefDialogService(mainStage);
        Injector.setModelOrService(DialogService.class, dialogService);

        stateManager.getRunningBackgroundTasks().addListener((ListChangeListener<? super Task<?>>) change -> {
            while (change.next()) {
                if (change.wasAdded()) {
                    for (Task<?> task : change.getAddedSubList()) {
                        dialogService.notify(new Notifications.TaskNotification(task));
                    }
                }
            }
        });

        JabRefGUI.clipBoardManager = new ClipBoardManager(stateManager);
        Injector.setModelOrService(ClipBoardManager.class, clipBoardManager);

        JabRefGUI.aiService = new AiService(
                preferences.getAiPreferences(),
                preferences.getFilePreferences(),
                dialogService,
                taskExecutor);
        Injector.setModelOrService(AiService.class, aiService);

        JabRefGUI.citationsAndRelationsSearchService = new SearchCitationsRelationsService(
                preferences.getImporterPreferences(),
                preferences.getImportFormatPreferences(),
                preferences.getFieldPreferences(),
                preferences.getEntryEditorPreferences().citationFetcherTypeProperty(),
                preferences.getEntryEditorPreferences().citationCountFetcherTypeProperty(),
                preferences.getCitationKeyPatternPreferences(),
                preferences.getGrobidPreferences(),
                preferences.getAiPreferences(),
                aiService.getCurrentChatModel(),
                entryTypesManager,
                dialogService
        );
        Injector.setModelOrService(SearchCitationsRelationsService.class, citationsAndRelationsSearchService);

        loadBundledFonts();

        JabRefGUI.bibTeXSyntaxHighlighter = new BibTeXSyntaxHighlighter();
        Injector.setModelOrService(BibTeXSyntaxHighlighter.class, bibTeXSyntaxHighlighter);
    }

    /// Registers bundled fonts so they can be referenced by family name in CSS.
    private void loadBundledFonts() {
        try (InputStream stream = JabRefGUI.class.getClassLoader().getResourceAsStream(BIBTEX_EDITOR_FONT_RESOURCE)) {
            if (stream == null) {
                LOGGER.warn("Could not find bundled font {}", BIBTEX_EDITOR_FONT_RESOURCE);
                return;
            }

            Font font = Font.loadFont(stream, -1);
            if (font == null) {
                LOGGER.warn("Could not load bundled font {}, falling back to the platform default", BIBTEX_EDITOR_FONT_RESOURCE);
            }
        } catch (IOException e) {
            LOGGER.warn("Could not load bundled font {}", BIBTEX_EDITOR_FONT_RESOURCE, e);
        }
    }

    private void setupProxy() {
        if (!preferences.getProxyPreferences().shouldUseProxy()) {
            return;
        }

        if (!preferences.getProxyPreferences().shouldUseAuthentication()) {
            ProxyRegisterer.register(preferences.getProxyPreferences());
            return;
        }

        assert preferences.getProxyPreferences().shouldUseAuthentication();

        if (preferences.getProxyPreferences().shouldPersistPassword()
                && StringUtil.isNotBlank(preferences.getProxyPreferences().getPassword())) {
            ProxyRegisterer.register(preferences.getProxyPreferences());
            return;
        }

        Optional<String> password = dialogService.showPasswordDialogAndWait(
                Localization.lang("Proxy configuration"),
                Localization.lang("Proxy requires password"),
                Localization.lang("Password"));

        if (password.isPresent()) {
            preferences.getProxyPreferences().setPassword(password.get());
            ProxyRegisterer.register(preferences.getProxyPreferences());
        } else {
            LOGGER.warn("No proxy password specified");
        }
    }

    private void openWindow() {
        LOGGER.debug("Initializing frame");

        CoreGuiPreferences coreGuiPreferences = preferences.getGuiPreferences();
        LOGGER.debug("Reading from prefs: isMaximized {}", coreGuiPreferences.isWindowMaximised());

        mainStage.setMinWidth(580);
        mainStage.setMinHeight(330);

        // maximized target state is stored, because "saveWindowState" saves x and y only if not maximized
        boolean windowMaximised = coreGuiPreferences.isWindowMaximised();

        LOGGER.debug("Screens: {}", Screen.getScreens());
        debugLogWindowState(mainStage);

        if (isWindowPositionInBounds()) {
            LOGGER.debug("The JabRef window is inside screen bounds.");
            mainStage.setX(coreGuiPreferences.getPositionX());
            mainStage.setY(coreGuiPreferences.getPositionY());
            mainStage.setWidth(coreGuiPreferences.getSizeX());
            mainStage.setHeight(coreGuiPreferences.getSizeY());
            LOGGER.debug("NOT saving window positions");
        } else {
            LOGGER.info("The JabRef window is outside of screen bounds. Position and size will be corrected to 1024x768. Primary screen will be used.");
            Rectangle2D bounds = Screen.getPrimary().getBounds();
            mainStage.setX(bounds.getMinX());
            mainStage.setY(bounds.getMinY());
            mainStage.setWidth(Math.min(bounds.getWidth(), 1024.0));
            mainStage.setHeight(Math.min(bounds.getHeight(), 768.0));
            LOGGER.debug("Saving window positions");
            saveWindowState();
        }
        // after calling "saveWindowState" the maximized state can be set
        mainStage.setMaximized(windowMaximised);
        debugLogWindowState(mainStage);

        PowerPane powerpane = new PowerPane();
        powerpane.getInfoCenterPane().getInfoCenterView().getGroups().addAll(dialogService.getNotificationGroups());
        Injector.setModelOrService(InfoCenterPane.class, powerpane.getInfoCenterPane());
        powerpane.setContent(JabRefGUI.mainFrame);
        powerpane.getInfoCenterPane().setInfoCenterViewPos(InfoCenterViewPos.BOTTOM_RIGHT);
        powerpane.getInfoCenterPane().autoHideProperty().bind(Bindings.isEmpty(dialogService.getPersistentNotifications()));

        Scene scene = new Scene(powerpane);
        installControlsFxDecorationPane(powerpane);

        LOGGER.debug("installing CSS");
        themeManager.installCssOnScene(scene);

        LOGGER.debug("Handle TextEditor key bindings");
        scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
            TextInputKeyBindings.call(scene, event, preferences.getKeyBindingRepository());
            SelectableTextFlowKeyBindings.call(scene, event, preferences.getKeyBindingRepository());
            WalkthroughKeyBindings.call(event, stateManager, preferences.getKeyBindingRepository());
        });

        mainStage.setTitle(JabRefFrame.FRAME_TITLE);
        mainStage.getIcons().addAll(IconTheme.getLogoSet());
        mainStage.setScene(scene);
        mainStage.setOnShowing(this::onShowing);
        mainStage.setOnCloseRequest(this::onCloseRequest);
        mainStage.setOnHiding(this::onHiding);

        LOGGER.debug("Showing mainStage");
        mainStage.show();

        LOGGER.debug("frame initialized");

        Platform.runLater(() -> mainFrame.handleUiCommands(uiCommands));

        // Lifecycle note: after this method, #onShowing will be called
    }

    // [impl->req~entry-editor.validation-decoration.startup~1]

    /// Installs ControlsFX's decoration root before validation is first used.
    ///
    /// ControlsFX injects its internal `DecorationPane` as the scene root on the first
    /// decoration. Doing that during startup avoids replacing the root while opening the entry
    /// editor, which would otherwise trigger a large CSS reapplication on the critical path.
    private static void installControlsFxDecorationPane(PowerPane powerpane) {
        Region probeNode = new Region();
        GraphicDecoration probeDecoration = new GraphicDecoration(probeNode);
        Decorator.addDecoration(powerpane, probeDecoration);
        Decorator.removeDecoration(powerpane, probeDecoration);
    }

    public void onShowing(WindowEvent event) {
        Platform.runLater(() -> {
            mainFrame.updateHorizontalDividerPosition();
            mainFrame.updateVerticalDividerPosition();
        });

        // Open last edited databases
        if (uiCommands.stream().noneMatch(UiCommand.BlankWorkspace.class::isInstance)
                && preferences.getWorkspacePreferences().shouldOpenLastEdited()) {
            mainFrame.openLastEditedDatabases();
        }

        Platform.runLater(() -> {
            // We need to check at this point, because here, all libraries are loaded (e.g., load previously opened libraries) and all UI commands (e.g., load libraries, blank workspace, ...) are handled.
            if (stateManager.getOpenDatabases().isEmpty()) {
                mainFrame.showWelcomeTab();
            }
        });
    }

    public void onCloseRequest(WindowEvent event) {
        if (!mainFrame.close()) {
            event.consume();
        }
    }

    public void onHiding(WindowEvent event) {
        saveWindowState();
        preferences.flush();
        Platform.exit();
    }

    private void saveWindowState() {
        CoreGuiPreferences preferences = JabRefGUI.preferences.getGuiPreferences();
        // workaround for mac, maximize will always report true
        if (!mainStage.isMaximized() || OS.OS_X) {
            preferences.setPositionX(mainStage.getX());
            preferences.setPositionY(mainStage.getY());
            preferences.setSizeX(mainStage.getWidth());
            preferences.setSizeY(mainStage.getHeight());
        }
        // maximize does not correctly work on OSX, reports true, although the window was resized!
        if (OS.OS_X) {
            preferences.setWindowMaximised(false);
        } else {
            preferences.setWindowMaximised(mainStage.isMaximized());
        }
        debugLogWindowState(mainStage);
    }

    /// prints the data from the screen (only in debug mode)
    ///
    /// @param mainStage JabRef's stage
    private void debugLogWindowState(Stage mainStage) {
        LOGGER.debug("""
                        screen data:
                          mainStage.WINDOW_MAXIMISED: {}
                          mainStage.POS_X: {}
                          mainStage.POS_Y: {}
                          mainStage.SIZE_X: {}
                          mainStage.SIZE_Y: {}
                        """,
                mainStage.isMaximized(), mainStage.getX(), mainStage.getY(), mainStage.getWidth(), mainStage.getHeight());
    }

    /// Tests if the window coordinates are inside any screen
    private boolean isWindowPositionInBounds() {
        CoreGuiPreferences coreGuiPreferences = preferences.getGuiPreferences();

        if (LOGGER.isDebugEnabled()) {
            Screen.getScreens().forEach(screen -> LOGGER.debug("Screen bounds: {}", screen.getBounds()));
        }

        return lowerLeftIsInBounds(coreGuiPreferences) && upperRightIsInBounds(coreGuiPreferences);
    }

    private boolean lowerLeftIsInBounds(CoreGuiPreferences coreGuiPreferences) {
        // Windows/PowerToys somehow removes 10 pixels to the left; they are re-added
        double leftX = coreGuiPreferences.getPositionX() + 10.0;
        double bottomY = coreGuiPreferences.getPositionY() + coreGuiPreferences.getSizeY();
        LOGGER.debug("left x: {}, bottom y: {}", leftX, bottomY);

        boolean inBounds = Screen.getScreens().stream().anyMatch(screen -> screen.getBounds().contains(leftX, bottomY));
        LOGGER.debug("lower left corner is in bounds: {}", inBounds);
        return inBounds;
    }

    private boolean upperRightIsInBounds(CoreGuiPreferences coreGuiPreferences) {
        // The upper right corner is checked as there are most probably the window controls.
        // Windows/PowerToys somehow adds 10 pixels to the right and top of the screen, they are removed
        double rightX = coreGuiPreferences.getPositionX() + coreGuiPreferences.getSizeX() - 10.0;
        double topY = coreGuiPreferences.getPositionY();
        LOGGER.debug("right x: {}, top y: {}", rightX, topY);

        boolean inBounds = Screen.getScreens().stream().anyMatch(screen -> screen.getBounds().contains(rightX, topY));
        LOGGER.debug("upper right corner is in bounds: {}", inBounds);
        return inBounds;
    }

    // Background tasks
    public void startBackgroundTasks() {
        RemotePreferences remotePreferences = preferences.getRemotePreferences();
        CLIMessageHandler cliMessageHandler = new CLIMessageHandler(mainFrame, preferences);
        if (remotePreferences.shouldEnableRemoteServer()) {
            remoteListenerServerManager.openAndStart(
                    cliMessageHandler,
                    remotePreferences.getRemoteServerPort());
        }

        if (remotePreferences.shouldEnableHttpServer()) {
            httpServerManager.start(preferences, stateManager, mainFrame, remotePreferences.getHttpServerUri());
        }
        if (remotePreferences.shouldEnableLanguageServer()) {
            languageServerController.start(cliMessageHandler, remotePreferences.getLanguageServerPort());
        }
    }

    private void setupHttpServerEnabledListener() {
        RemotePreferences remotePreferences = preferences.getRemotePreferences();
        EasyBind.listen(remotePreferences.enableHttpServerProperty(), (_, _, newValue) -> {
            // stop in all cases, because the port might have changed
            httpServerManager.stop();
            if (newValue) {
                httpServerManager.start(preferences, stateManager, mainFrame, remotePreferences.getHttpServerUri());
            }
        });
    }

    @Override
    public void stop() {
        LOGGER.trace("Stopping JabRef GUI");
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            LOGGER.trace("Stopping JabRef GUI using a virtual thread executor");

            // Shutdown everything in parallel to prevent causing non-shutdown of something in case of issues

            executor.submit(() -> {
                LOGGER.trace("Closing citations and relations search service");
                citationsAndRelationsSearchService.close();
                LOGGER.trace("Citations and relations search service closed");
            });

            executor.submit(() -> {
                LOGGER.trace("Closing AI service");
                try {
                    aiService.close();
                } catch (Exception e) {
                    LOGGER.error("Unable to close AI service", e);
                }
                LOGGER.trace("AI service closed");
            });

            executor.submit(() -> {
                LOGGER.trace("Closing OpenOffice connection");
                OOBibBaseConnect.closeOfficeConnection();
                LOGGER.trace("OpenOffice connection closed");
            });

            executor.submit(() -> {
                LOGGER.trace("Shutting down remote server manager");
                remoteListenerServerManager.stop();
                LOGGER.trace("RemoteListenerServerManager shut down");
            });

            executor.submit(() -> {
                LOGGER.trace("Shutting down http server manager");
                httpServerManager.stop();
                LOGGER.trace("HttpServerManager shut down");
            });

            executor.submit(() -> {
                LOGGER.trace("Shutting down LSP server controller");
                languageServerController.stop();
                LOGGER.trace("LanguageServerController shut down");
            });

            executor.submit(() -> {
                LOGGER.trace("Stopping background tasks");
                Unirest.shutDown();
                LOGGER.trace("Unirest shut down");
            });

            // region All threading related shutdowns
            executor.submit(() -> {
                LOGGER.trace("Shutting down taskExecutor");
                if (taskExecutor != null) {
                    taskExecutor.shutdown();
                }
                LOGGER.trace("TaskExecutor shut down");
            });

            executor.submit(() -> {
                LOGGER.trace("Shutting down fileUpdateMonitor");
                fileUpdateMonitor.shutdown();
                LOGGER.trace("FileUpdateMonitor shut down");
            });

            executor.submit(() -> {
                LOGGER.trace("Shutting down directoryMonitor");
                DirectoryMonitor directoryMonitor = Injector.instantiateModelOrService(DirectoryMonitor.class);
                directoryMonitor.shutdown();
                LOGGER.trace("DirectoryMonitor shut down");
            });

            executor.submit(() -> {
                if (preferences.getSearchPreferences().shouldUsePostgresSearch()) {
                    LOGGER.trace("Shutting down postgreServer");
                    PostgresServer postgresServer = Injector.instantiateModelOrService(PostgresServer.class);
                    postgresServer.close();
                    LOGGER.trace("PostgreServer shut down");
                }
            });

            executor.submit(() -> {
                LOGGER.trace("Shutting down HeadlessExecutorService");
                HeadlessExecutorService.INSTANCE.shutdownEverything();
                LOGGER.trace("HeadlessExecutorService shut down");
            });
            // endregion

            HeadlessExecutorService.gracefullyShutdown("HeadlessExecutorService", executor, 30);
        }

        LOGGER.trace("Finished stop");

        // Just to be sure that we do not leave any threads running
        System.exit(0);
    }
}
