package ctbrec.ui.settings;

import static ctbrec.Settings.DirectoryStructure.*;
import static ctbrec.Settings.ProxyType.*;

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.Hmac;
import ctbrec.Settings;
import ctbrec.Settings.DirectoryStructure;
import ctbrec.Settings.ProxyType;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
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.tabs.TabSelectionListener;
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.scene.control.Tab;
import javafx.scene.control.TextInputDialog;
import javafx.scene.layout.GridPane;

public class SettingsTab extends Tab implements TabSelectionListener {

    private static final Logger LOG = LoggerFactory.getLogger(SettingsTab.class);
    public static final int CHECKBOX_MARGIN = 6;

    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 livePreviews;
    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 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 SimpleLongProperty leaveSpaceOnDevice;
    private SimpleIntegerProperty minimumLengthInSecs;
    private SimpleStringProperty ffmpegParameters;
    private SimpleStringProperty fileExtension;
    private SimpleStringProperty server;
    private SimpleIntegerProperty port;
    private SimpleStringProperty path;
    private SimpleBooleanProperty requireAuthentication;
    private SimpleBooleanProperty transportLayerSecurity;
    private ExclusiveSelectionProperty recordLocal;
    private SimpleFileProperty postProcessing;
    private SimpleIntegerProperty postProcessingThreads;
    private SimpleBooleanProperty removeRecordingAfterPp;
    private IgnoreList ignoreList;

    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);
        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, "splitRecordings", FXCollections.observableList(getSplitOptions()));
        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));
        minimumLengthInSecs = new SimpleIntegerProperty(null, "minimumLengthInSeconds", settings.minimumLengthInSeconds);
        ffmpegParameters = new SimpleStringProperty(null, "ffmpegMergedDownloadArgs", settings.ffmpegMergedDownloadArgs);
        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);
        requireAuthentication = new SimpleBooleanProperty(null, "requireAuthentication", settings.requireAuthentication);
        requireAuthentication.addListener(this::requireAuthenticationChanged);
        transportLayerSecurity = new SimpleBooleanProperty(null, "transportLayerSecurity", settings.transportLayerSecurity);
        recordLocal = new ExclusiveSelectionProperty(null, "localRecording", settings.localRecording, "Local", "Remote");
        postProcessing = new SimpleFileProperty(null, "postProcessing", settings.postProcessing);
        postProcessingThreads = new SimpleIntegerProperty(null, "postProcessingThreads", settings.postProcessingThreads);
        removeRecordingAfterPp = new SimpleBooleanProperty(null, "removeRecordingAfterPostProcessing", settings.removeRecordingAfterPostProcessing);
    }

    private void createGui() {
        ignoreList = new IgnoreList(sites);
        List<Category> siteCategories = new ArrayList<>();
        for (Site site : sites) {
            siteCategories.add(Category.of(site.getName(), SiteUiFactory.getUi(site).getConfigUI().createConfigPanel()));
        }

        Preferences prefs = Preferences.of(new CtbrecPreferencesStorage(config),
                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"),
                                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("Enable live previews (experimental)", livePreviews),
                                Setting.of("Start Tab", startTab),
                                Setting.of("Colors (Base / Accent)", new ColorSettingsPane(Config.getInstance()))
                                ),
                        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 (minutes)", splitAfter).converter(SplitAfterOption.converter()),
                                Setting.of("Manually select stream quality", chooseStreamQuality, "Opens a dialog to select the video resolution before recording"),
                                Setting.of("Restrict Resolution", resolutionRange, "Only record streams with resolution within the given range"),
                                Setting.of("Concurrent Recordings (0 = unlimited)", concurrentRecordings),
                                Setting.of("Check online state every (seconds)", onlineCheckIntervalInSecs, "Check every x seconds, if a model came online"),
                                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")
                                ),
                        Group.of("Location",
                                Setting.of("Record Location", recordLocal),
                                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("Require authentication", requireAuthentication),
                                Setting.of("Use Secure Communication (TLS)", transportLayerSecurity)
                                )
                        ),
                Category.of("Post-Processing",
                        Group.of("Post-Processing",
                                Setting.of("Post-Processing", postProcessing),
                                Setting.of("Threads", postProcessingThreads),
                                Setting.of("Delete recordings shorter than (secs)", minimumLengthInSecs, "Delete recordings, which are shorter than x seconds. 0 to disable"),
                                Setting.of("Remove recording after post-processing", removeRecordingAfterPp)
                                )
                        ),
                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),
                                Setting.of("Host", proxyHost),
                                Setting.of("Port", proxyPort),
                                Setting.of("Username", proxyUser),
                                Setting.of("Password", proxyPassword)
                                )
                        )
                );
        setContent(prefs.getView());
        prefs.expandTree();
    }

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

    private List<SplitAfterOption> getSplitOptions() {
        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 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() {
        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() {
        // TODO restartLabel.setVisible(true);
    }

    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);
                }
            };
        }
    }
}
