package ctbrec.ui.settings;

import static ctbrec.Settings.DirectoryStructure.*;
import static ctbrec.Settings.ProxyType.*;
import static ctbrec.Settings.SplitStrategy.*;
import static java.util.Optional.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Hmac;
import ctbrec.Settings;
import ctbrec.Settings.DirectoryStructure;
import ctbrec.Settings.ProxyType;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SiteUI;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.controls.range.DiscreteRange;
import ctbrec.ui.settings.api.Category;
import ctbrec.ui.settings.api.ExclusiveSelectionProperty;
import ctbrec.ui.settings.api.GigabytesConverter;
import ctbrec.ui.settings.api.Group;
import ctbrec.ui.settings.api.Preferences;
import ctbrec.ui.settings.api.Setting;
import ctbrec.ui.settings.api.SimpleDirectoryProperty;
import ctbrec.ui.settings.api.SimpleFileProperty;
import ctbrec.ui.settings.api.SimpleRangeProperty;
import ctbrec.ui.settings.api.ValueConverter;
import ctbrec.ui.sites.ConfigUI;
import ctbrec.ui.tabs.TabSelectionListener;
import javafx.animation.FadeTransition;
import javafx.animation.PauseTransition;
import javafx.animation.Transition;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TextInputDialog;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.util.Duration;

public class SettingsTab extends Tab implements TabSelectionListener {

    private static final Logger LOG = LoggerFactory.getLogger(SettingsTab.class);
    public static final int CHECKBOX_MARGIN = 6;
    private static final long MiB = 1024 * 1024L;
    private static final long GiB = 1024 * MiB;

    private List<Site> sites;
    private Recorder recorder;
    private boolean initialized = false;
    private Config config;
    private Settings settings;

    private SimpleStringProperty httpUserAgent;
    private SimpleStringProperty httpUserAgentMobile;
    private SimpleIntegerProperty overviewUpdateIntervalInSecs;
    private SimpleBooleanProperty updateThumbnails;
    private SimpleBooleanProperty determineResolution;
    private SimpleBooleanProperty chooseStreamQuality;
    private SimpleBooleanProperty confirmationDialogs;
    private SimpleBooleanProperty livePreviews;
    private SimpleBooleanProperty monitorClipboard;
    private SimpleListProperty<String> startTab;
    private SimpleFileProperty mediaPlayer;
    private SimpleStringProperty mediaPlayerParams;
    private SimpleIntegerProperty maximumResolutionPlayer;
    private SimpleBooleanProperty showPlayerStarting;
    private SimpleBooleanProperty singlePlayer;
    private SimpleListProperty<ProxyType> proxyType;
    private SimpleStringProperty proxyHost;
    private SimpleStringProperty proxyPort;
    private SimpleStringProperty proxyUser;
    private SimpleStringProperty proxyPassword;
    private SimpleDirectoryProperty recordingsDir;
    private SimpleListProperty<DirectoryStructure> directoryStructure;
    private SimpleListProperty<SplitAfterOption> splitAfter;
    private SimpleListProperty<SplitBiggerThanOption> splitBiggerThan;
    private SimpleRangeProperty<Integer> resolutionRange;
    private List<Integer> labels = Arrays.asList(0, 240, 360, 480, 600, 720, 960, 1080, 1440, 2160, 4320, 8640);
    private List<Integer> values = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11);
    private DiscreteRange<Integer> rangeValues = new DiscreteRange<>(values, labels);
    private SimpleIntegerProperty concurrentRecordings;
    private SimpleIntegerProperty onlineCheckIntervalInSecs;
    private SimpleBooleanProperty onlineCheckSkipsPausedModels;
    private SimpleLongProperty leaveSpaceOnDevice;
    private SimpleStringProperty ffmpegParameters;
    private SimpleBooleanProperty logFFmpegOutput;
    private SimpleBooleanProperty loghlsdlOutput;
    private SimpleStringProperty fileExtension;
    private SimpleStringProperty server;
    private SimpleIntegerProperty port;
    private SimpleStringProperty path;
    private SimpleStringProperty downloadFilename;
    private SimpleBooleanProperty requireAuthentication;
    private SimpleBooleanProperty totalModelCountInTitle;
    private SimpleBooleanProperty transportLayerSecurity;
    private SimpleBooleanProperty fastScrollSpeed;
    private SimpleBooleanProperty useHlsdl;
    private SimpleBooleanProperty recentlyWatched;
    private SimpleFileProperty hlsdlExecutable;
    private ExclusiveSelectionProperty recordLocal;
    private SimpleIntegerProperty postProcessingThreads;
    private IgnoreList ignoreList;
    private Label restartNotification;
    private SimpleIntegerProperty playlistRequestTimeout;

    public SettingsTab(List<Site> sites, Recorder recorder) {
        this.sites = sites;
        this.recorder = recorder;
        setText("Settings");
        setClosable(false);
        config = Config.getInstance();
        settings = config.getSettings();
    }

    private void initializeProperties() {
        httpUserAgent = new SimpleStringProperty(null, "httpUserAgent", settings.httpUserAgent);
        httpUserAgentMobile = new SimpleStringProperty(null, "httpUserAgentMobile", settings.httpUserAgentMobile);
        overviewUpdateIntervalInSecs = new SimpleIntegerProperty(null, "overviewUpdateIntervalInSecs", settings.overviewUpdateIntervalInSecs);
        updateThumbnails = new SimpleBooleanProperty(null, "updateThumbnails", settings.updateThumbnails);
        determineResolution = new SimpleBooleanProperty(null, "determineResolution", settings.determineResolution);
        chooseStreamQuality = new SimpleBooleanProperty(null, "chooseStreamQuality", settings.chooseStreamQuality);
        livePreviews = new SimpleBooleanProperty(null, "livePreviews", settings.livePreviews);
        monitorClipboard = new SimpleBooleanProperty(null, "monitorClipboard", settings.monitorClipboard);
        startTab = new SimpleListProperty<>(null, "startTab", FXCollections.observableList(getTabNames()));
        mediaPlayer = new SimpleFileProperty(null, "mediaPlayer", settings.mediaPlayer);
        mediaPlayerParams = new SimpleStringProperty(null, "mediaPlayerParams", settings.mediaPlayerParams);
        maximumResolutionPlayer = new SimpleIntegerProperty(null, "maximumResolutionPlayer", settings.maximumResolutionPlayer);
        showPlayerStarting = new SimpleBooleanProperty(null, "showPlayerStarting", settings.showPlayerStarting);
        singlePlayer = new SimpleBooleanProperty(null, "singlePlayer", settings.singlePlayer);
        proxyType = new SimpleListProperty<>(null, "proxyType", FXCollections.observableList(List.of(DIRECT, HTTP, SOCKS4, SOCKS5)));
        proxyHost = new SimpleStringProperty(null, "proxyHost", settings.proxyHost);
        proxyPort = new SimpleStringProperty(null, "proxyPort", settings.proxyPort);
        proxyUser = new SimpleStringProperty(null, "proxyUser", settings.proxyUser);
        proxyPassword = new SimpleStringProperty(null, "proxyPassword", settings.proxyPassword);
        recordingsDir = new SimpleDirectoryProperty(null, "recordingsDir", settings.recordingsDir);
        directoryStructure = new SimpleListProperty<>(null, "recordingsDirStructure", FXCollections.observableList(List.of(FLAT, ONE_PER_MODEL, ONE_PER_RECORDING)));
        splitAfter = new SimpleListProperty<>(null, "splitRecordingsAfterSecs", FXCollections.observableList(getSplitAfterSecsOptions()));
        splitBiggerThan = new SimpleListProperty<>(null, "splitRecordingsBiggerThanBytes", FXCollections.observableList(getSplitBiggerThanOptions()));
        resolutionRange = new SimpleRangeProperty<>(rangeValues, "minimumResolution", "maximumResolution", settings.minimumResolution, settings.maximumResolution);
        concurrentRecordings = new SimpleIntegerProperty(null, "concurrentRecordings", settings.concurrentRecordings);
        onlineCheckIntervalInSecs = new SimpleIntegerProperty(null, "onlineCheckIntervalInSecs", settings.onlineCheckIntervalInSecs);
        leaveSpaceOnDevice = new SimpleLongProperty(null, "minimumSpaceLeftInBytes",  (long) new GigabytesConverter().convertTo(settings.minimumSpaceLeftInBytes));
        ffmpegParameters = new SimpleStringProperty(null, "ffmpegMergedDownloadArgs", settings.ffmpegMergedDownloadArgs);
        logFFmpegOutput = new SimpleBooleanProperty(null, "logFFmpegOutput", settings.logFFmpegOutput);
        loghlsdlOutput = new SimpleBooleanProperty(null, "loghlsdlOutput", settings.loghlsdlOutput);
        fileExtension = new SimpleStringProperty(null, "ffmpegFileSuffix", settings.ffmpegFileSuffix);
        server = new SimpleStringProperty(null, "httpServer", settings.httpServer);
        port = new SimpleIntegerProperty(null, "httpPort", settings.httpPort);
        path = new SimpleStringProperty(null, "servletContext", settings.servletContext);
        downloadFilename = new SimpleStringProperty(null, "downloadFilename", settings.downloadFilename);
        requireAuthentication = new SimpleBooleanProperty(null, "requireAuthentication", settings.requireAuthentication);
        requireAuthentication.addListener(this::requireAuthenticationChanged);
        totalModelCountInTitle = new SimpleBooleanProperty(null, "totalModelCountInTitle", settings.totalModelCountInTitle);
        transportLayerSecurity = new SimpleBooleanProperty(null, "transportLayerSecurity", settings.transportLayerSecurity);
        recordLocal = new ExclusiveSelectionProperty(null, "localRecording", settings.localRecording, "Local", "Remote");
        postProcessingThreads = new SimpleIntegerProperty(null, "postProcessingThreads", settings.postProcessingThreads);
        onlineCheckSkipsPausedModels = new SimpleBooleanProperty(null, "onlineCheckSkipsPausedModels", settings.onlineCheckSkipsPausedModels);
        fastScrollSpeed = new SimpleBooleanProperty(null, "fastScrollSpeed", settings.fastScrollSpeed);
        confirmationDialogs = new SimpleBooleanProperty(null, "confirmationForDangerousActions", settings.confirmationForDangerousActions);
        useHlsdl = new SimpleBooleanProperty(null, "useHlsdl", settings.useHlsdl);
        hlsdlExecutable = new SimpleFileProperty(null, "hlsdlExecutable", settings.hlsdlExecutable);
        recentlyWatched = new SimpleBooleanProperty(null, "recentlyWatched", settings.recentlyWatched);
        playlistRequestTimeout = new SimpleIntegerProperty(null, "playlistRequestTimeout", settings.playlistRequestTimeout);
    }

    private void createGui() {
        PostProcessingStepPanel postProcessingStepPanel = new PostProcessingStepPanel(config);
        Button variablesHelpButton = createHelpButton("Variables", "http://localhost:5689/docs/PostProcessing.md#variables");
        ignoreList = new IgnoreList(sites);
        List<Category> siteCategories = new ArrayList<>();
        for (Site site : sites) {
            ofNullable(SiteUiFactory.getUi(site)).map(SiteUI::getConfigUI).map(ConfigUI::createConfigPanel)
            .ifPresent(configPanel -> siteCategories.add(Category.of(site.getName(), configPanel)));
        }

        CtbrecPreferencesStorage storage = new CtbrecPreferencesStorage(config);
        Preferences prefs = Preferences.of(storage,
                Category.of("General",
                        Group.of("General",
                                Setting.of("User-Agent", httpUserAgent),
                                Setting.of("User-Agent mobile", httpUserAgentMobile),
                                Setting.of("Update overview interval (seconds)", overviewUpdateIntervalInSecs, "Update the thumbnail overviews every x seconds").needsRestart(),
                                Setting.of("Update thumbnails", updateThumbnails, "The overviews will still be updated, but the thumbnails won't be changed. This is useful for less powerful systems."),
                                Setting.of("Display stream resolution in overview", determineResolution),
                                Setting.of("Manually select stream quality", chooseStreamQuality, "Opens a dialog to select the video resolution before recording"),
                                Setting.of("Enable live previews (experimental)", livePreviews),
                                Setting.of("Enable recently watched tab", recentlyWatched).needsRestart(),
                                Setting.of("Add models from clipboard", monitorClipboard, "Monitor clipboard for model URLs and automatically add them to the recorder").needsRestart(),
                                Setting.of("Fast scroll speed", fastScrollSpeed, "Makes the thumbnail overviews scroll faster with the mouse wheel").needsRestart(),
                                Setting.of("Show confirmation dialogs", confirmationDialogs, "Show confirmation dialogs for irreversible actions"),
                                Setting.of("Total model count in title", totalModelCountInTitle, "Show the total number of models in the title bar"),
                                Setting.of("Start Tab", startTab),
                                Setting.of("Colors (Base / Accent)", new ColorSettingsPane(Config.getInstance())).needsRestart()
                                ),
                        Group.of("Player",
                                Setting.of("Player", mediaPlayer),
                                Setting.of("Start parameters", mediaPlayerParams),
                                Setting.of("Maximum resolution (0 = unlimited)", maximumResolutionPlayer, "video height, e.g. 720 or 1080"),
                                Setting.of("Show \"Player Starting\" Message", showPlayerStarting),
                                Setting.of("Start only one player at a time", singlePlayer)
                                )
                        ),
                Category.of("Recorder",
                        Group.of("Settings",
                                Setting.of("Recordings Directory", recordingsDir),
                                Setting.of("Directory Structure", directoryStructure),
                                Setting.of("Split recordings after", splitAfter).converter(SplitAfterOption.converter()).onChange(this::splitValuesChanged),
                                Setting.of("Split recordings bigger than", splitBiggerThan).converter(SplitBiggerThanOption.converter()).onChange(this::splitValuesChanged),
                                Setting.of("Restrict Resolution", resolutionRange, "Only record streams with resolution within the given range"),
                                Setting.of("Concurrent Recordings (0 = unlimited)", concurrentRecordings),
                                Setting.of("Leave space on device (GiB)", leaveSpaceOnDevice, "Stop recording, if the free space on the device gets below this threshold").converter(new GigabytesConverter()),
                                Setting.of("FFmpeg parameters", ffmpegParameters, "FFmpeg parameters to use when merging stream segments"),
                                Setting.of("File Extension", fileExtension, "File extension to use for recordings"),
                                Setting.of("Check online state every (seconds)", onlineCheckIntervalInSecs, "Check every x seconds, if a model came online"),
                                Setting.of("Skip online check for paused models", onlineCheckSkipsPausedModels, "Skip online check for paused models")
                                ),
                        Group.of("Location",
                                Setting.of("Record Location", recordLocal).needsRestart(),
                                Setting.of("Server", server),
                                Setting.of("Port", port),
                                Setting.of("Path", path, "Leave empty, if you didn't change the servletContext in the server config"),
                                Setting.of("Download Filename", downloadFilename, "File name pattern for downloads"),
                                Setting.of("", variablesHelpButton),
                                Setting.of("Require authentication", requireAuthentication),
                                Setting.of("Use Secure Communication (TLS)", transportLayerSecurity)
                                )
                        ),
                Category.of("Post-Processing",
                        Group.of("Post-Processing",
                                Setting.of("Threads", postProcessingThreads),
                                Setting.of("Steps", postProcessingStepPanel),
                                Setting.of("", createHelpButton("Post-Processing Help", "http://localhost:5689/docs/PostProcessing.md"))
                                )
                        ),
                Category.of("Events & Actions", new ActionSettingsPanel(recorder)),
                Category.of("Ignore List", ignoreList),
                Category.of("Sites", siteCategories.toArray(new Category[0])),
                Category.of("Proxy",
                        Group.of("Proxy",
                                Setting.of("Type", proxyType).needsRestart(),
                                Setting.of("Host", proxyHost).needsRestart(),
                                Setting.of("Port", proxyPort).needsRestart(),
                                Setting.of("Username", proxyUser).needsRestart(),
                                Setting.of("Password", proxyPassword).needsRestart()
                                )
                        ),
                Category.of("Advanced / Devtools",
                        Group.of("Networking",
                                Setting.of("Playlist request timeout (ms)", playlistRequestTimeout, "Timeout in ms for playlist requests")
                                ),
                        Group.of("Logging",
                                Setting.of("Log FFmpeg output", logFFmpegOutput, "Log FFmpeg output to files in the system's temp directory")
                                ),
                        Group.of("hlsdl (experimental)",
                                Setting.of("Use hlsdl (if possible)", useHlsdl, "Use hlsdl to record the live streams. Some features might not work correctly."),
                                Setting.of("hlsdl executable", hlsdlExecutable, "Path to the hlsdl executable"),
                                Setting.of("Log hlsdl output", loghlsdlOutput, "Log hlsdl output to files in the system's temp directory")
                                )
                        )
                );
        Region preferencesView = prefs.getView();
        prefs.onRestartRequired(this::showRestartRequired);
        storage.setPreferences(prefs);
        preferencesView.setMinSize(800, 400);
        preferencesView.setPrefSize(1280, 960);
        ScrollPane scrollPane = new ScrollPane(preferencesView);
        GridPane container = new GridPane();
        container.add(scrollPane, 0, 0);
        GridPane.setFillWidth(scrollPane, true);
        GridPane.setFillHeight(scrollPane, true);
        GridPane.setHgrow(scrollPane, Priority.ALWAYS);
        GridPane.setVgrow(scrollPane, Priority.ALWAYS);

        StackPane stackPane = new StackPane();
        stackPane.getChildren().add(container);
        restartNotification = new Label("Restart Required");
        restartNotification.setVisible(false);
        restartNotification.setOpacity(0);
        restartNotification.setStyle("-fx-font-size: 28; -fx-padding: .3em");
        restartNotification.setBorder(new Border(new BorderStroke(Color.web(settings.colorAccent), BorderStrokeStyle.SOLID, new CornerRadii(5), new BorderWidths(2))));
        restartNotification.setBackground(new Background(new BackgroundFill(Color.web(settings.colorBase), new CornerRadii(5), Insets.EMPTY)));
        stackPane.getChildren().add(restartNotification);
        StackPane.setAlignment(restartNotification, Pos.TOP_RIGHT);
        StackPane.setMargin(restartNotification, new Insets(10, 40, 0, 0));

        setContent(stackPane);
        prefs.expandTree();

        prefs.getSetting("httpServer").ifPresent(s -> bindEnabledProperty(s, recordLocal));
        prefs.getSetting("httpPort").ifPresent(s -> bindEnabledProperty(s, recordLocal));
        prefs.getSetting("servletContext").ifPresent(s -> bindEnabledProperty(s, recordLocal));
        prefs.getSetting("requireAuthentication").ifPresent(s -> bindEnabledProperty(s, recordLocal));
        prefs.getSetting("transportLayerSecurity").ifPresent(s -> bindEnabledProperty(s, recordLocal));
        prefs.getSetting("recordingsDir").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
        prefs.getSetting("splitRecordingsAfterSecs").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
        prefs.getSetting("splitRecordingsBiggerThanBytes").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
        prefs.getSetting("minimumResolution").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
        prefs.getSetting("recordingsDirStructure").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
        prefs.getSetting("onlineCheckIntervalInSecs").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
        prefs.getSetting("onlineCheckSkipsPausedModels").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
        prefs.getSetting("minimumSpaceLeftInBytes").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
        prefs.getSetting("postProcessing").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
        prefs.getSetting("postProcessingThreads").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
        prefs.getSetting("removeRecordingAfterPostProcessing").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
        prefs.getSetting("minimumLengthInSeconds").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
        prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
        prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
        prefs.getSetting("downloadFilename").ifPresent(s -> bindEnabledProperty(s, recordLocal));
        prefs.getSetting("hlsdlExecutable").ifPresent(s -> bindEnabledProperty(s, useHlsdl.not()));
        prefs.getSetting("loghlsdlOutput").ifPresent(s -> bindEnabledProperty(s, useHlsdl.not()));
        postProcessingStepPanel.disableProperty().bind(recordLocal.not());
        variablesHelpButton.disableProperty().bind(recordLocal);
    }

    private void splitValuesChanged(ObservableValue<?> value, Object oldV, Object newV) {
        boolean splitAfterSet = settings.splitRecordingsAfterSecs > 0;
        boolean splitBiggerThanSet = settings.splitRecordingsBiggerThanBytes > 0;
        if (splitAfterSet && splitBiggerThanSet) {
            settings.splitStrategy = TIME_OR_SIZE;
        } else if (splitAfterSet) {
            settings.splitStrategy = TIME;
        } else if (splitBiggerThanSet) {
            settings.splitStrategy = SIZE;
        } else {
            settings.splitStrategy = DONT;
        }
        saveConfig();
    }

    private Button createHelpButton(String text, String url) {
        Button postProcessingHelpButton = new Button(text);
        postProcessingHelpButton.setOnAction(e -> DesktopIntegration.open(url));
        return postProcessingHelpButton;
    }

    private void bindEnabledProperty(Setting s, BooleanExpression bindTo) {
        try {
            s.getGui().disableProperty().bind(bindTo);
        } catch (Exception e) {
            LOG.error("Couldn't bind disableProperty of {}", s.getName(), e);
        }
    }

    private List<String> getTabNames() {
        return getTabPane().getTabs().stream()
                .map(Tab::getText)
                .collect(Collectors.toList());
    }

    private List<SplitAfterOption> getSplitAfterSecsOptions() {
        List<SplitAfterOption> splitOptions = new ArrayList<>();
        splitOptions.add(new SplitAfterOption("disabled", 0));
        if (Config.isDevMode()) {
            splitOptions.add(new SplitAfterOption("1 min", 1 * 60));
            splitOptions.add(new SplitAfterOption("3 min", 3 * 60));
        }
        splitOptions.add(new SplitAfterOption("5 min", 5 * 60));
        splitOptions.add(new SplitAfterOption("10 min", 10 * 60));
        splitOptions.add(new SplitAfterOption("15 min", 15 * 60));
        splitOptions.add(new SplitAfterOption("20 min", 20 * 60));
        splitOptions.add(new SplitAfterOption("30 min", 30 * 60));
        splitOptions.add(new SplitAfterOption("60 min", 60 * 60));
        return splitOptions;
    }

    private List<SplitBiggerThanOption> getSplitBiggerThanOptions() {
        List<SplitBiggerThanOption> splitOptions = new ArrayList<>();
        splitOptions.add(new SplitBiggerThanOption("disabled", 0));
        if (Config.isDevMode()) {
            splitOptions.add(new SplitBiggerThanOption("10 MiB", 10 * MiB));
            splitOptions.add(new SplitBiggerThanOption("20 MiB", 20 * MiB));
        }
        splitOptions.add(new SplitBiggerThanOption("100 MiB", 100 * MiB));
        splitOptions.add(new SplitBiggerThanOption("250 MiB", 250 * MiB));
        splitOptions.add(new SplitBiggerThanOption("500 MiB", 500 * MiB));
        splitOptions.add(new SplitBiggerThanOption("1 GiB", 1 * GiB));
        splitOptions.add(new SplitBiggerThanOption("2 GiB", 2 * GiB));
        splitOptions.add(new SplitBiggerThanOption("3 GiB", 3 * GiB));
        splitOptions.add(new SplitBiggerThanOption("4 GiB", 4 * GiB));
        splitOptions.add(new SplitBiggerThanOption("5 GiB", 5 * GiB));
        splitOptions.add(new SplitBiggerThanOption("6 GiB", 6 * GiB));
        splitOptions.add(new SplitBiggerThanOption("7 GiB", 7 * GiB));
        splitOptions.add(new SplitBiggerThanOption("8 GiB", 8 * GiB));
        splitOptions.add(new SplitBiggerThanOption("9 GiB", 9 * GiB));
        splitOptions.add(new SplitBiggerThanOption("10 GiB", 10 * GiB));
        return splitOptions;
    }

    private void requireAuthenticationChanged(ObservableValue<?> obs, Boolean oldV, Boolean newV) { // NOSONAR
        boolean requiresAuthentication = newV;
        Config.getInstance().getSettings().requireAuthentication = requiresAuthentication;
        if (requiresAuthentication) {
            byte[] key = Config.getInstance().getSettings().key;
            if (key == null) {
                key = Hmac.generateKey();
                Config.getInstance().getSettings().key = key;
                saveConfig();
            }
            TextInputDialog keyDialog = new TextInputDialog();
            keyDialog.setResizable(true);
            keyDialog.setTitle("Server Authentication");
            keyDialog.setHeaderText("A key has been generated");
            keyDialog.setContentText("Add this setting to your server's config.json:\n");
            keyDialog.getEditor().setText("\"key\": " + Arrays.toString(key));
            keyDialog.getEditor().setEditable(false);
            keyDialog.setWidth(800);
            keyDialog.setHeight(200);
            keyDialog.show();
        }
    }

    public void saveConfig() {
        GlobalThreadPool.submit(() -> {
            try {
                Config.getInstance().save();
            } catch (IOException e) {
                LOG.error("Couldn't save config", e);
            }
        });
    }

    @Override
    public void selected() {
        if (!initialized) {
            initializeProperties();
            createGui();
            initialized = true;
        }
        ignoreList.refresh();
    }

    @Override
    public void deselected() {
        saveConfig();
    }

    public static GridPane createGridLayout() {
        GridPane layout = new GridPane();
        layout.setPadding(new Insets(10));
        layout.setHgap(5);
        layout.setVgap(5);
        return layout;
    }

    void showRestartRequired() {
        if (!restartNotification.isVisible()) {
            restartNotification.setVisible(true);
            Transition fadeIn = changeOpacity(restartNotification, 1);
            fadeIn.play();
            fadeIn.setOnFinished(e -> {
                Transition fadeOut = changeOpacity(restartNotification, 0);
                fadeOut.setOnFinished(e2 -> restartNotification.setVisible(false));
                PauseTransition pauseTransition = new PauseTransition(Duration.seconds(5));
                pauseTransition.setOnFinished(evt -> fadeOut.play());
                pauseTransition.play();
            });
        }
    }

    private static final Duration ANIMATION_DURATION = new Duration(500);
    private Transition changeOpacity(Node node, double opacity) {
        FadeTransition transition = new FadeTransition(ANIMATION_DURATION, node);
        transition.setFromValue(node.getOpacity());
        transition.setToValue(opacity);
        return transition;
    }

    public static class SplitAfterOption {
        private String label;
        private int value;

        public SplitAfterOption(String label, int value) {
            super();
            this.label = label;
            this.value = value;
        }

        public int getValue() {
            return value;
        }

        @Override
        public String toString() {
            return label;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + value;
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            SplitAfterOption other = (SplitAfterOption) obj;
            return value == other.value;
        }

        public static ValueConverter converter() {
            return new ValueConverter() {
                @Override
                public Integer convertFrom(Object splitAfterOption) {
                    return ((SplitAfterOption) splitAfterOption).getValue();
                }

                @Override
                public SplitAfterOption convertTo(Object integer) {
                    return new SplitAfterOption(integer.toString(), (Integer) integer);
                }
            };
        }
    }

    public static class SplitBiggerThanOption {
        private String label;
        private long value;

        public SplitBiggerThanOption(String label, long value) {
            super();
            this.label = label;
            this.value = value;
        }

        public long getValue() {
            return value;
        }

        @Override
        public String toString() {
            return label;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + (int) (value ^ (value >>> 32));
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            SplitBiggerThanOption other = (SplitBiggerThanOption) obj;
            return value == other.value;
        }

        public static ValueConverter converter() {
            return new ValueConverter() {
                @Override
                public Long convertFrom(Object splitBiggerThanOption) {
                    return ((SplitBiggerThanOption) splitBiggerThanOption).getValue();
                }

                @Override
                public SplitBiggerThanOption convertTo(Object value) {
                    return new SplitBiggerThanOption(value.toString(), (Long) value);
                }
            };
        }
    }
}
