package ctbrec.ui.tabs;

import static ctbrec.Recording.State.*;
import static javafx.scene.control.ButtonType.*;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.nio.file.NoSuchFileException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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

import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.Recording.State;
import ctbrec.StringUtil;
import ctbrec.recorder.ProgressListener;
import ctbrec.recorder.Recorder;
import ctbrec.recorder.RecordingPinnedException;
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
import ctbrec.sites.Site;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.CamrecApplication;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.FileDownload;
import ctbrec.ui.JavaFxRecording;
import ctbrec.ui.Player;
import ctbrec.ui.controls.DateTimeCellFactory;
import ctbrec.ui.controls.Toast;
import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Tab;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
import javafx.stage.FileChooser;
import javafx.util.Duration;

public class RecordingsTab extends Tab implements TabSelectionListener {
    private static final String ERROR_WHILE_DOWNLOADING_RECORDING = "Error while downloading recording";

    private static final Logger LOG = LoggerFactory.getLogger(RecordingsTab.class);

    private ScheduledService<List<JavaFxRecording>> updateService;
    private Config config;
    private Recorder recorder;
    @SuppressWarnings("unused")
    private List<Site> sites;
    private long spaceTotal = -1;
    private long spaceFree = -1;

    FlowPane grid = new FlowPane();
    ScrollPane scrollPane = new ScrollPane();
    TableView<JavaFxRecording> table = new TableView<>();
    ObservableList<JavaFxRecording> observableRecordings = FXCollections.observableArrayList();
    ContextMenu popup;
    ProgressBar spaceLeft;
    Label spaceLabel;
    Lock recordingsLock = new ReentrantLock();

    public RecordingsTab(String title, Recorder recorder, Config config, List<Site> sites) {
        super(title);
        this.recorder = recorder;
        this.config = config;
        this.sites = sites;
        createGui();
        setClosable(false);
        initializeUpdateService();
    }

    @SuppressWarnings("unchecked")
    private void createGui() {
        grid.setPadding(new Insets(5));
        grid.setHgap(5);
        grid.setVgap(5);

        scrollPane.setContent(grid);
        scrollPane.setFitToHeight(true);
        scrollPane.setFitToWidth(true);
        BorderPane.setMargin(scrollPane, new Insets(5));

        table.setEditable(false);
        table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        TableColumn<JavaFxRecording, String> name = new TableColumn<>("Model");
        name.setId("name");
        name.setPrefWidth(200);
        name.setCellValueFactory(cdf -> new SimpleStringProperty(cdf.getValue().getModel().getName()));
        TableColumn<JavaFxRecording, Instant> date = new TableColumn<>("Date");
        date.setId("date");
        date.setCellValueFactory(cdf -> {
            Instant instant = cdf.getValue().getStartDate();
            return new SimpleObjectProperty<Instant>(instant);
        });
        date.setCellFactory(new DateTimeCellFactory<JavaFxRecording>());
        date.setPrefWidth(200);
        TableColumn<JavaFxRecording, String> status = new TableColumn<>("Status");
        status.setId("status");
        status.setCellValueFactory(cdf -> cdf.getValue().getStatusProperty());
        status.setPrefWidth(300);
        TableColumn<JavaFxRecording, String> progress = new TableColumn<>("Progress");
        progress.setId("progress");
        progress.setCellValueFactory(cdf -> cdf.getValue().getProgressProperty());
        progress.setPrefWidth(100);
        TableColumn<JavaFxRecording, Number> size = new TableColumn<>("Size");
        size.setId("size");
        size.setStyle("-fx-alignment: CENTER-RIGHT;");
        size.setPrefWidth(100);
        size.setCellValueFactory(cdf -> cdf.getValue().getSizeProperty());
        size.setCellFactory(param -> createSizeCell());

        table.getColumns().addAll(name, date, status, progress, size);
        table.setItems(observableRecordings);
        table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, this::onContextMenuRequested);
        table.addEventHandler(MouseEvent.MOUSE_PRESSED, this::onMousePressed);
        table.addEventFilter(MouseEvent.MOUSE_CLICKED, this::onMouseClicked);
        table.addEventFilter(KeyEvent.KEY_PRESSED, this::onKeyPressed);
        scrollPane.setContent(table);

        HBox spaceBox = new HBox(5);
        Label l = new Label("Space left on device");
        HBox.setMargin(l, new Insets(2, 0, 0, 0));
        spaceBox.getChildren().add(l);
        spaceLeft = new ProgressBar(0);
        spaceLeft.setPrefSize(200, 22);
        spaceLabel = new Label();
        spaceLabel.setFont(Font.font(11));
        StackPane stack = new StackPane(spaceLeft, spaceLabel);
        spaceBox.getChildren().add(stack);
        BorderPane.setMargin(spaceBox, new Insets(5));

        BorderPane root = new BorderPane();
        root.setPadding(new Insets(5));
        root.setTop(spaceBox);
        root.setCenter(scrollPane);
        setContent(root);

        restoreState();
    }

    public boolean isDownloadRunning() {
        return observableRecordings.stream()
                .map(Recording::getStatus)
                .anyMatch(s -> s == DOWNLOADING);
    }

    private TableCell<JavaFxRecording, Number> createSizeCell() {
        TableCell<JavaFxRecording, Number> cell = new TableCell<JavaFxRecording, Number>() {
            @Override
            protected void updateItem(Number sizeInByte, boolean empty) {
                if(empty || sizeInByte == null) {
                    setText(null);
                    setStyle(null);
                } else {
                    setText(StringUtil.formatSize(sizeInByte));
                    setStyle("-fx-alignment: CENTER-RIGHT;");
                    if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
                        int row = this.getTableRow().getIndex();
                        JavaFxRecording rec = tableViewProperty().get().getItems().get(row);
                        if(!rec.valueChanged() && rec.getStatus() == RECORDING) {
                            setStyle("-fx-alignment: CENTER-RIGHT; -fx-background-color: red");
                        }
                    }
                }
            }
        };
        return cell;
    }

    private void onContextMenuRequested(ContextMenuEvent event) {
        List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
        if (recordings != null && !recordings.isEmpty()) {
            popup = createContextMenu(recordings);
            if (!popup.getItems().isEmpty()) {
                popup.show(table, event.getScreenX(), event.getScreenY());
            }
        }
        event.consume();
    }

    private void onMousePressed(MouseEvent event) {
        if(popup != null) {
            popup.hide();
        }
    }

    private void onMouseClicked(MouseEvent event) {
        if(event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
            Recording recording = table.getSelectionModel().getSelectedItem();
            if(recording != null) {
                play(recording);
            }
        }
    }

    private void onKeyPressed( KeyEvent event ) {
        List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
        if (recordings != null && !recordings.isEmpty()) {
            State status = recordings.get(0).getStatus();
            if (event.getCode() == KeyCode.DELETE) {
                if(recordings.size() > 1 || status == FINISHED || status == FAILED || status == WAITING) {
                    delete(recordings);
                }
            } else if (event.getCode() == KeyCode.ENTER && status == FINISHED) {
                play(recordings.get(0));
            } else {
                jumpToNextModel(event.getCode());
            }
        }
    }

    void initializeUpdateService() {
        updateService = createUpdateService();
        updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
        updateService.setOnSucceeded(event -> {
            updateRecordingsTable();
            updateFreeSpaceDisplay();
        });
        updateService.setOnFailed(event -> {
            LOG.info("Couldn't get list of recordings from recorder", event.getSource().getException());
            AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR, getTabPane().getScene());
            autosizeAlert.setTitle("Whoopsie!");
            autosizeAlert.setHeaderText("Recordings not available");
            autosizeAlert.setContentText("An error occured while retrieving the list of recordings");
            autosizeAlert.showAndWait();
        });
    }

    private void updateFreeSpaceDisplay() {
        if(spaceTotal != -1 && spaceFree != -1) {
            double free = ((double)spaceFree) / spaceTotal;
            spaceLeft.setProgress(free);
            double totalGiB = ((double) spaceTotal) / 1024 / 1024 / 1024;
            double freeGiB = ((double) spaceFree) / 1024 / 1024 / 1024;
            DecimalFormat df = new DecimalFormat("0.00");
            String tt = df.format(freeGiB) + " / " + df.format(totalGiB) + " GiB";
            spaceLeft.setTooltip(new Tooltip(tt));
            spaceLabel.setText(tt);
        }
    }

    private void updateRecordingsTable() {
        List<JavaFxRecording> recordings = updateService.getValue();
        if (recordings == null) {
            return;
        }

        recordingsLock.lock();
        try {
            for (Iterator<JavaFxRecording> iterator = observableRecordings.iterator(); iterator.hasNext();) {
                JavaFxRecording old = iterator.next();
                if (!recordings.contains(old)) {
                    // remove deleted recordings
                    iterator.remove();
                }
            }
            for (JavaFxRecording recording : recordings) {
                if (!observableRecordings.contains(recording)) {
                    // add new recordings
                    observableRecordings.add(recording);
                } else {
                    // update existing ones
                    int index = observableRecordings.indexOf(recording);
                    JavaFxRecording old = observableRecordings.get(index);
                    old.update(recording);
                }
            }
        } finally {
            recordingsLock.unlock();
        }
        table.sort();
    }

    private ScheduledService<List<JavaFxRecording>> createUpdateService() {
        ScheduledService<List<JavaFxRecording>>  service = new ScheduledService<List<JavaFxRecording>>() {
            @Override
            protected Task<List<JavaFxRecording>> createTask() {
                return new Task<List<JavaFxRecording>>() {
                    @Override
                    public List<JavaFxRecording> call() throws IOException, InvalidKeyException, NoSuchAlgorithmException {
                        updateSpace();

                        List<JavaFxRecording> recordings = new ArrayList<>();
                        for (Recording rec : recorder.getRecordings()) {
                            recordings.add(new JavaFxRecording(rec));
                        }
                        return recordings;
                    }

                    private void updateSpace() {
                        try {
                            spaceTotal = recorder.getTotalSpaceBytes();
                            spaceFree = recorder.getFreeSpaceBytes();
                            Platform.runLater(() -> spaceLeft.setTooltip(new Tooltip()));
                        } catch (NoSuchFileException e) {
                            // recordings dir does not exist
                            Platform.runLater(() -> spaceLeft.setTooltip(new Tooltip("Recordings directory does not exist")));
                        } catch (IOException e) {
                            LOG.error("Couldn't update free space", e);
                        }
                    }
                };
            }
        };
        ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
            Thread t = new Thread(r);
            t.setDaemon(true);
            t.setName("RecordingsTab UpdateService");
            return t;
        });
        service.setExecutor(executor);
        return service;
    }

    @Override
    public void selected() {
        if (updateService != null) {
            updateService.reset();
            updateService.restart();
        }
    }

    @Override
    public void deselected() {
        if (updateService != null) {
            updateService.cancel();
        }
    }

    private ContextMenu createContextMenu(List<JavaFxRecording> recordings) {
        ContextMenu contextMenu = new ContextMenu();
        contextMenu.setHideOnEscape(true);
        contextMenu.setAutoHide(true);
        contextMenu.setAutoFix(true);

        JavaFxRecording first = recordings.get(0);
        MenuItem openInPlayer = new MenuItem("Open in Player");
        openInPlayer.setOnAction(e -> play(recordings.get(0)));
        if(first.getStatus() == FINISHED || Config.getInstance().getSettings().localRecording) {
            contextMenu.getItems().add(openInPlayer);
        }

        // TODO find a way to reenable this
        //        MenuItem stopRecording = new MenuItem("Stop recording");
        //        stopRecording.setOnAction((e) -> {
        //            Model m = site.createModel(recording.getModelName());
        //            try {
        //                recorder.stopRecording(m);
        //            } catch (Exception e1) {
        //                showErrorDialog("Stop recording", "Couldn't stop recording of model " + m.getName(), e1);
        //            }
        //        });
        //        if(recording.getStatus() == STATUS.RECORDING) {
        //            contextMenu.getItems().add(stopRecording);
        //        }

        MenuItem deleteRecording = new MenuItem("Delete");
        deleteRecording.setOnAction(e -> delete(recordings));
        if(first.getStatus() == FINISHED || first.getStatus() == WAITING || first.getStatus() == FAILED || recordings.size() > 1) {
            contextMenu.getItems().add(deleteRecording);
            deleteRecording.setDisable(recordings.stream().allMatch(Recording::isPinned));
        }

        MenuItem openDir = new MenuItem("Open directory");
        openDir.setOnAction(e -> onOpenDirectory(first));
        if(Config.getInstance().getSettings().localRecording) {
            contextMenu.getItems().add(openDir);
        }

        MenuItem downloadRecording = new MenuItem("Download");
        downloadRecording.setOnAction(e -> download(first));
        if (!Config.getInstance().getSettings().localRecording && first.getStatus() == FINISHED) {
            contextMenu.getItems().add(downloadRecording);
        }

        if (first.isPinned()) {
            MenuItem unpinRecording = new MenuItem("Unpin");
            unpinRecording.setOnAction(e -> unpin(recordings));
            contextMenu.getItems().add(unpinRecording);
        } else {
            MenuItem pinRecording = new MenuItem("Pin");
            pinRecording.setOnAction(e -> pin(recordings));
            contextMenu.getItems().add(pinRecording);
        }

        MenuItem rerunPostProcessing = new MenuItem("Rerun Post-Processing");
        rerunPostProcessing.setOnAction(e -> triggerPostProcessing(first));
        if (first.getStatus() == FAILED || first.getStatus() == WAITING || first.getStatus() == FINISHED) {
            contextMenu.getItems().add(rerunPostProcessing);
        }

        if(recordings.size() > 1) {
            openInPlayer.setDisable(true);
            openDir.setDisable(true);
            rerunPostProcessing.setDisable(true);
        }

        return contextMenu;
    }

    private void pin(List<JavaFxRecording> recordings) {
        table.setCursor(Cursor.WAIT);
        Thread backgroundThread = new Thread(() -> {
            List<Exception> exceptions = new ArrayList<>();
            try {
                for (JavaFxRecording javaFxRecording : recordings) {
                    Recording rec = javaFxRecording.getDelegate();
                    try {
                        recorder.pin(rec);
                        javaFxRecording.setPinned(true);
                    } catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
                        exceptions.add(e);
                    }
                }
            } finally {
                Platform.runLater(() -> {
                    table.setCursor(Cursor.DEFAULT);
                    if (!exceptions.isEmpty()) {
                        showErrorDialog("Error while pinning recordings", "At least one recording couldn't be pinned", exceptions);
                    }
                });
            }
        });
        backgroundThread.start();
    }

    private void unpin(List<JavaFxRecording> recordings) {
        table.setCursor(Cursor.WAIT);
        Thread backgroundThread = new Thread(() -> {
            List<Exception> exceptions = new ArrayList<>();
            try {
                for (JavaFxRecording javaFxRecording : recordings) {
                    Recording rec = javaFxRecording.getDelegate();
                    try {
                        recorder.unpin(rec);
                        javaFxRecording.setPinned(false);
                    } catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
                        exceptions.add(e);
                    }
                }
            } finally {
                Platform.runLater(() -> {
                    table.setCursor(Cursor.DEFAULT);
                    if (!exceptions.isEmpty()) {
                        showErrorDialog("Error while unpinning recordings", "At least one recording couldn't be unpinned", exceptions);
                    }
                });
            }
        });
        backgroundThread.start();
    }

    private void jumpToNextModel(KeyCode code) {
        if (!table.getItems().isEmpty() && (code.isLetterKey() || code.isDigitKey())) {
            // determine where to start looking for the next model
            int startAt = 0;
            if (table.getSelectionModel().getSelectedIndex() >= 0) {
                startAt = table.getSelectionModel().getSelectedIndex() + 1;
                if (startAt >= table.getItems().size()) {
                    startAt = 0;
                }
            }

            String c = code.getChar().toLowerCase();
            int i = startAt;
            do {
                JavaFxRecording current = table.getItems().get(i);
                if (current.getModel().getName().toLowerCase().replaceAll("[^0-9a-z]", "").startsWith(c)) {
                    table.getSelectionModel().clearAndSelect(i);
                    table.scrollTo(i);
                    break;
                }

                i++;
                if (i >= table.getItems().size()) {
                    i = 0;
                }
            } while (i != startAt);
        }
    }

    private void onOpenDirectory(JavaFxRecording first) {
        String recordingsDir = Config.getInstance().getSettings().recordingsDir;
        String path = first.getPath();
        File tsFile = new File(recordingsDir, path);
        new Thread(() -> DesktopIntegration.open(tsFile.getParent())).start();
    }

    private void triggerPostProcessing(JavaFxRecording first) {
        new Thread(() -> {
            try {
                recorder.rerunPostProcessing(first.getDelegate());
            } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
                showErrorDialog("Error while starting post-processing", "The post-processing could not be started", e1);
                LOG.error("Error while starting post-processing", e1);
            }
        }).start();
    }

    private void download(Recording recording) {
        LOG.debug("Path {}", recording.getPath());
        String filename = proposeTargetFilename(recording);
        FileChooser chooser = new FileChooser();
        chooser.setInitialFileName(filename);
        if(config.getSettings().lastDownloadDir != null && !config.getSettings().lastDownloadDir.equals("")) {
            File dir = new File(config.getSettings().lastDownloadDir);
            while(!dir.exists()) {
                dir = dir.getParentFile();
            }
            chooser.setInitialDirectory(dir);
        }
        File target = chooser.showSaveDialog(null);
        if(target != null) {
            config.getSettings().lastDownloadDir = target.getParent();
            startDownloadThread(target, recording);
            recording.setStatus(DOWNLOADING);
            recording.setProgress(0);
        }
    }

    private String proposeTargetFilename(Recording recording) {
        String path = recording.getPath().substring(1);
        if(recording.isSingleFile()) {
            return new File(path).getName();
        } else {
            String fileSuffix = config.getSettings().ffmpegFileSuffix;
            String filename = path.replace("/", "-").replace(".mp4", "") + '.' + fileSuffix;
            return filename;
        }
    }

    private void startDownloadThread(File target, Recording recording) {
        Thread t = new Thread(() -> {
            try {
                String hlsBase = config.getServerUrl() + "/hls";
                if (recording.isSingleFile()) {
                    URL url = new URL(hlsBase + recording.getPath());
                    FileDownload download = new FileDownload(CamrecApplication.httpClient, createDownloadListener(recording));
                    download.start(url, target);
                } else {
                    URL url = new URL(hlsBase + recording.getPath() + "/playlist.m3u8");
                    MergedFfmpegHlsDownload download = new MergedFfmpegHlsDownload(CamrecApplication.httpClient);
                    download.init(config, recording.getModel(), Instant.now());
                    LOG.info("Downloading {}", url);
                    download.downloadFinishedRecording(url.toString(), target, createDownloadListener(recording), recording.getSizeInByte());
                }
            } catch (FileNotFoundException e) {
                showErrorDialog(ERROR_WHILE_DOWNLOADING_RECORDING, "The target file couldn't be created", e);
                LOG.error(ERROR_WHILE_DOWNLOADING_RECORDING, e);
            } catch (Exception e) {
                showErrorDialog(ERROR_WHILE_DOWNLOADING_RECORDING, "The recording could not be downloaded", e);
                LOG.error(ERROR_WHILE_DOWNLOADING_RECORDING, e);
            } finally {
                Platform.runLater(() -> {
                    recording.setStatus(FINISHED);
                    recording.setProgress(-1);
                });
            }
        });
        t.setDaemon(true);
        t.setName("Download Thread " + recording.getPath());
        t.start();
    }

    private ProgressListener createDownloadListener(Recording recording) {
        return progress -> Platform.runLater(() -> {
            if (progress == 100) {
                recording.setStatus(FINISHED);
                recording.setProgress(-1);
                LOG.debug("Download finished for recording {}", recording.getPath());
            } else {
                recording.setStatus(DOWNLOADING);
                recording.setProgress(progress);
            }
        });
    }

    private void showErrorDialog(final String title, final String msg, final Exception e) {
        showErrorDialog(title, msg, Collections.singletonList(e));
    }

    private void showErrorDialog(final String title, final String msg, final List<Exception> exceptions) {
        Platform.runLater(() -> {
            AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR, getTabPane().getScene());
            autosizeAlert.setTitle(title);
            autosizeAlert.setHeaderText(msg);
            StringBuilder contentText = new StringBuilder("On or more error(s) occured:");
            for (Exception exception : exceptions) {
                contentText.append("\n• ").append(exception.getLocalizedMessage());
            }
            autosizeAlert.setContentText(contentText.toString());
            autosizeAlert.showAndWait();
        });
    }

    private void play(Recording recording) {
        new Thread() {
            @Override
            public void run() {
                boolean started = Player.play(recording);
                if(started && Config.getInstance().getSettings().showPlayerStarting) {
                    Platform.runLater(() -> Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500));
                }
            }
        }.start();
    }

    private void delete(List<JavaFxRecording> recordings) {
        table.setCursor(Cursor.WAIT);
        String msg;
        if(recordings.size() > 1) {
            msg = "Delete " + recordings.size() + " recordings for good?";
        } else {
            Recording r = recordings.get(0);
            msg = "Delete " + r.getModel().getName() + "/" + r.getStartDate() + " for good?";
        }
        AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, msg, getTabPane().getScene(), YES, NO);
        confirm.setTitle("Delete recording?");
        confirm.setHeaderText(msg);
        confirm.setContentText("");
        confirm.showAndWait();
        if (confirm.getResult() == ButtonType.YES) {
            deleteAsync(recordings);
        } else {
            table.setCursor(Cursor.DEFAULT);
        }
    }

    private void deleteAsync(List<JavaFxRecording> recordings) {
        Thread deleteThread = new Thread(() -> {
            recordingsLock.lock();
            try {
                List<Recording> deleted = new ArrayList<>();
                List<Exception> exceptions = new ArrayList<>();
                for (Iterator<JavaFxRecording> iterator = recordings.iterator(); iterator.hasNext();) {
                    JavaFxRecording r =  iterator.next();
                    if (r.getStatus() != FINISHED && r.getStatus() != FAILED && r.getStatus() != WAITING) {
                        continue;
                    }
                    try {
                        recorder.delete(r.getDelegate());
                        deleted.add(r);
                    } catch (RecordingPinnedException | IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
                        exceptions.add(e1);
                        LOG.error("Error while deleting recording", e1);
                    }
                }
                if (!exceptions.isEmpty()) {
                    showErrorDialog("Error while deleting recording", "Recording not deleted", exceptions);
                }
                observableRecordings.removeAll(deleted);
            } finally {
                recordingsLock.unlock();
                Platform.runLater(() -> table.setCursor(Cursor.DEFAULT));
            }
        });
        deleteThread.start();
    }

    public void saveState() {
        if(!table.getSortOrder().isEmpty()) {
            TableColumn<JavaFxRecording, ?> col = table.getSortOrder().get(0);
            Config.getInstance().getSettings().recordingsSortColumn = col.getText();
            Config.getInstance().getSettings().recordingsSortType = col.getSortType().toString();
        }
        int columns = table.getColumns().size();
        double[] columnWidths = new double[columns];
        String[] columnIds = new String[columns];
        for (int i = 0; i < columnWidths.length; i++) {
            columnWidths[i] = table.getColumns().get(i).getWidth();
            columnIds[i] = table.getColumns().get(i).getId();
        }
        Config.getInstance().getSettings().recordingsColumnWidths = columnWidths;
        Config.getInstance().getSettings().recordingsColumnIds = columnIds;
    }

    private void restoreState() {
        restoreColumnOrder();
        restoreColumnWidths();
        restoreSorting();
    }

    private void restoreSorting() {
        String sortCol = Config.getInstance().getSettings().recordingsSortColumn;
        if(StringUtil.isNotBlank(sortCol)) {
            for (TableColumn<JavaFxRecording, ?> col : table.getColumns()) {
                if(Objects.equals(sortCol, col.getText())) {
                    col.setSortType(SortType.valueOf(Config.getInstance().getSettings().recordingsSortType));
                    table.getSortOrder().clear();
                    table.getSortOrder().add(col);
                    break;
                }
            }
        }
    }

    private void restoreColumnOrder() {
        String[] columnIds = Config.getInstance().getSettings().recordingsColumnIds;
        ObservableList<TableColumn<JavaFxRecording,?>> columns = table.getColumns();
        for (int i = 0; i < columnIds.length; i++) {
            for (int j = 0; j < table.getColumns().size(); j++) {
                if(Objects.equals(columnIds[i], columns.get(j).getId())) {
                    TableColumn<JavaFxRecording, ?> col = columns.get(j);
                    columns.remove(j); // NOSONAR
                    columns.add(i, col);
                }
            }
        }
    }

    private void restoreColumnWidths() {
        double[] columnWidths = Config.getInstance().getSettings().recordingsColumnWidths;
        if(columnWidths != null && columnWidths.length == table.getColumns().size()) {
            for (int i = 0; i < columnWidths.length; i++) {
                table.getColumns().get(i).setPrefWidth(columnWidths[i]);
            }
        }
    }
}
