package ctbrec.ui.tabs;

import ctbrec.Config;
import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.JavaFxModel;
import ctbrec.ui.PreviewPopupHandler;
import ctbrec.ui.action.*;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.controls.autocomplete.AutoFillTextField;
import ctbrec.ui.controls.autocomplete.ObservableListSuggester;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.util.Callback;
import javafx.util.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
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.ReentrantLock;
import java.util.stream.Collectors;

public class RecordLaterTab extends Tab implements TabSelectionListener {
    private static final Logger LOG = LoggerFactory.getLogger(RecordLaterTab.class);

    private ReentrantLock lock = new ReentrantLock();
    private ScheduledService<List<JavaFxModel>> updateService;
    private Recorder recorder;
    private List<Site> sites;

    FlowPane grid = new FlowPane();
    ScrollPane scrollPane = new ScrollPane();
    TableView<JavaFxModel> table = new TableView<>();
    ObservableList<JavaFxModel> observableModels = FXCollections.observableArrayList();
    ObservableList<JavaFxModel> filteredModels = FXCollections.observableArrayList();
    ContextMenu popup;

    Label modelLabel = new Label("Model");
    AutoFillTextField model;
    Button addModelButton = new Button("Record");
    Button checkModelAccountExistance = new Button("Check URLs");
    TextField filter;

    public RecordLaterTab(String title, Recorder recorder, List<Site> sites) {
        super(title);
        this.recorder = recorder;
        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(true);
        table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        PreviewPopupHandler previewPopupHandler = new PreviewPopupHandler(table);
        table.setRowFactory(tableview -> {
            TableRow<JavaFxModel> row = new TableRow<>();
            row.addEventHandler(MouseEvent.ANY, previewPopupHandler);
            return row;
        });
        TableColumn<JavaFxModel, String> preview = new TableColumn<>("🎥");
        preview.setPrefWidth(35);
        preview.setCellValueFactory(cdf -> new SimpleStringProperty("  ▶  "));
        preview.setEditable(false);
        preview.setId("preview");
        if (!Config.getInstance().getSettings().livePreviews) {
            preview.setVisible(false);
        }
        TableColumn<JavaFxModel, String> name = new TableColumn<>("Model");
        name.setPrefWidth(200);
        name.setCellValueFactory(new PropertyValueFactory<>("displayName"));
        name.setCellFactory(new ClickableCellFactory<>());
        name.setEditable(false);
        name.setId("name");
        TableColumn<JavaFxModel, String> url = new TableColumn<>("URL");
        url.setCellValueFactory(new PropertyValueFactory<>("url"));
        url.setCellFactory(new ClickableCellFactory<>());
        url.setPrefWidth(400);
        url.setEditable(false);
        url.setId("url");
        TableColumn<JavaFxModel, String> notes = new TableColumn<>("Notes");
        notes.setCellValueFactory(cdf -> {
            JavaFxModel m = cdf.getValue();
            return new StringPropertyBase() {
                @Override
                public String getName() {
                    return "Model Notes";
                }

                @Override
                public Object getBean() {
                    return null;
                }

                @Override
                public String get() {
                    String modelNotes = Config.getInstance().getModelNotes(m);
                    return modelNotes;
                }
            };
        });
        notes.setPrefWidth(400);
        notes.setEditable(false);
        notes.setId("notes");
        table.getColumns().addAll(preview, name, url, notes);
        table.setItems(observableModels);
        table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
            popup = createContextMenu();
            if (popup != null) {
                popup.show(table, event.getScreenX(), event.getScreenY());
            }
            event.consume();
        });
        table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
            if (popup != null) {
                popup.hide();
            }
        });
        table.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
            List<JavaFxModel> selectedModels = table.getSelectionModel().getSelectedItems();
            if (event.getCode() == KeyCode.DELETE) {
                stopAction(selectedModels);
            } else {
                jumpToNextModel(event.getCode());
            }
        });

        scrollPane.setContent(table);

        HBox addModelBox = new HBox(5);
        modelLabel.setPadding(new Insets(5, 0, 0, 0));
        ObservableList<String> suggestions = FXCollections.observableArrayList();
        sites.forEach(site -> suggestions.add(site.getClass().getSimpleName()));
        model = new AutoFillTextField(new ObservableListSuggester(suggestions));
        model.minWidth(150);
        model.prefWidth(600);
        HBox.setHgrow(model, Priority.ALWAYS);
        model.setPromptText("e.g. MyFreeCams:ModelName or an URL like https://chaturbate.com/modelname/");
        model.onActionHandler(this::addModel);
        model.setTooltip(new Tooltip("To add a model enter SiteName:ModelName\n" + "press ENTER to confirm a suggested site name"));
        BorderPane.setMargin(addModelBox, new Insets(5));
        addModelButton.setOnAction(this::addModel);
        addModelButton.setPadding(new Insets(5));
        addModelBox.getChildren().addAll(modelLabel, model, addModelButton, checkModelAccountExistance);
        checkModelAccountExistance.setPadding(new Insets(5));
        checkModelAccountExistance.setTooltip(new Tooltip("Go over all model URLs and check, if the account still exists"));
        HBox.setMargin(checkModelAccountExistance, new Insets(0, 0, 0, 20));
        checkModelAccountExistance.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder)
                .execute(Model::isMarkedForLaterRecording));

        HBox filterContainer = new HBox();
        filterContainer.setSpacing(0);
        filterContainer.setPadding(new Insets(0));
        filterContainer.setAlignment(Pos.CENTER_RIGHT);
        filterContainer.minWidth(100);
        filterContainer.prefWidth(150);
        HBox.setHgrow(filterContainer, Priority.ALWAYS);
        filter = new SearchBox(false);
        filter.minWidth(100);
        filter.prefWidth(150);
        filter.setPromptText("Filter");
        filter.textProperty().addListener((observableValue, oldValue, newValue) -> {
            String q = filter.getText();
            lock.lock();
            try {
                filter(q);
            } finally {
                lock.unlock();
            }
        });
        filter.getStyleClass().remove("search-box-icon");
        filterContainer.getChildren().add(filter);
        addModelBox.getChildren().add(filterContainer);

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

        restoreState();
    }

    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 {
                JavaFxModel current = table.getItems().get(i);
                if (current.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 addModel(ActionEvent e) {
        String input = model.getText();
        if (StringUtil.isBlank(input)) {
            return;
        }

        if (input.startsWith("http")) {
            addModelByUrl(input);
        } else {
            addModelByName(input);
        }
    }

    private void addModelByUrl(String url) {
        for (Site site : sites) {
            Model newModel = site.createModelFromUrl(url);
            if (newModel != null) {
                try {
                    newModel.setMarkedForLaterRecording(true);
                    recorder.addModel(newModel);
                } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
                    Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + newModel.getName() + " could not be added: ", e1);
                }
                return;
            }
        }

        Dialogs.showError(getTabPane().getScene(), "Unknown URL format",
                "The URL you entered has an unknown format or the function does not support this site, yet", null);
    }

    private void addModelByName(String siteModelCombo) {
        String[] parts = siteModelCombo.trim().split(":");
        if (parts.length != 2) {
            Dialogs.showError(getTabPane().getScene(), "Wrong input format", "Use something like \"MyFreeCams:ModelName\"", null);
            return;
        }

        String siteName = parts[0];
        String modelName = parts[1];
        for (Site site : sites) {
            if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) {
                try {
                    Model m = site.createModel(modelName);
                    m.setMarkedForLaterRecording(true);
                    recorder.addModel(m);
                } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
                    Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + modelName + " could not be added:", e1);
                }
                return;
            }
        }

        Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getTabPane().getScene());
        alert.setTitle("Unknown site");
        alert.setHeaderText("Couldn't add model");
        alert.setContentText("The site you entered is unknown");
        alert.showAndWait();
    }

    void initializeUpdateService() {
        updateService = createUpdateService();
        updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
        updateService.setOnSucceeded(this::onUpdateSuccess);
        updateService.setOnFailed(event -> LOG.info("Couldn't get list of models from recorder", event.getSource().getException()));
    }

    private void onUpdateSuccess(WorkerStateEvent event) {
        List<JavaFxModel> updatedModels = updateService.getValue();
        if (updatedModels == null) {
            return;
        }

        lock.lock();
        try {
            addOrUpdateModels(updatedModels);

            // remove old ones, which are not in the list of updated models
            for (Iterator<JavaFxModel> iterator = observableModels.iterator(); iterator.hasNext();) {
                Model oldModel = iterator.next();
                if (!updatedModels.contains(oldModel)) {
                    iterator.remove();
                }
            }
        } finally {
            lock.unlock();
        }

        filteredModels.clear();
        filter(filter.getText());
        table.sort();
    }

    private void addOrUpdateModels(List<JavaFxModel> updatedModels) {
        for (JavaFxModel updatedModel : updatedModels) {
            int index = observableModels.indexOf(updatedModel);
            if (index == -1) {
                observableModels.add(updatedModel);
            } else {
                // make sure to update the JavaFX online property, so that the table cell is updated
                JavaFxModel oldModel = observableModels.get(index);
                oldModel.setSuspended(updatedModel.isSuspended());
                oldModel.getOnlineProperty().set(updatedModel.getOnlineProperty().get());
                oldModel.getRecordingProperty().set(updatedModel.getRecordingProperty().get());
                oldModel.lastRecordedProperty().set(updatedModel.lastRecordedProperty().get());
                oldModel.lastSeenProperty().set(updatedModel.lastSeenProperty().get());
            }
        }
    }

    private void filter(String filter) {
        lock.lock();
        try {
            if (StringUtil.isBlank(filter)) {
                observableModels.addAll(filteredModels);
                filteredModels.clear();
                return;
            }

            String[] tokens = filter.split(" ");
            observableModels.addAll(filteredModels);
            filteredModels.clear();
            for (int i = 0; i < table.getItems().size(); i++) {
                StringBuilder sb = new StringBuilder();
                for (TableColumn<JavaFxModel, ?> tc : table.getColumns()) {
                    Object cellData = tc.getCellData(i);
                    if (cellData != null) {
                        String content = cellData.toString();
                        sb.append(content).append(' ');
                    }
                }
                String searchText = sb.toString();

                boolean tokensMissing = false;
                for (String token : tokens) {
                    if (!searchText.toLowerCase().contains(token.toLowerCase())) {
                        tokensMissing = true;
                        break;
                    }
                }
                if (tokensMissing) {
                    JavaFxModel filteredModel = table.getItems().get(i);
                    filteredModels.add(filteredModel);
                }
            }
            observableModels.removeAll(filteredModels);
        } finally {
            lock.unlock();
        }
    }

    private ScheduledService<List<JavaFxModel>> createUpdateService() {
        ScheduledService<List<JavaFxModel>> modelUpdateService = new ScheduledService<List<JavaFxModel>>() {
            @Override
            protected Task<List<JavaFxModel>> createTask() {
                return new Task<List<JavaFxModel>>() {
                    @Override
                    public List<JavaFxModel> call() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
                        LOG.trace("Updating models marked for later recording");
                        return recorder.getModels().stream().filter(Model::isMarkedForLaterRecording).map(JavaFxModel::new).collect(Collectors.toList());
                    }
                };
            }
        };
        ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
            Thread t = new Thread(r);
            t.setDaemon(true);
            t.setName("RecordLaterTab UpdateService");
            return t;
        });
        modelUpdateService.setExecutor(executor);
        return modelUpdateService;
    }

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

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

    private ContextMenu createContextMenu() {
        ObservableList<JavaFxModel> selectedModels = table.getSelectionModel().getSelectedItems();
        if (selectedModels.isEmpty()) {
            return null;
        }
        MenuItem start = new MenuItem("Start Recording");
        start.setOnAction(e -> startAction(selectedModels));
        MenuItem stop = new MenuItem("Remove Model");
        stop.setOnAction(e -> stopAction(selectedModels));

        MenuItem copyUrl = new MenuItem("Copy URL");
        copyUrl.setOnAction(e -> {
            Model selected = selectedModels.get(0);
            final Clipboard clipboard = Clipboard.getSystemClipboard();
            final ClipboardContent content = new ClipboardContent();
            content.putString(selected.getUrl());
            clipboard.setContent(content);
        });

        MenuItem openInBrowser = new MenuItem("Open in Browser");
        openInBrowser.setOnAction(e -> DesktopIntegration.open(selectedModels.get(0).getUrl()));
        MenuItem openInPlayer = new MenuItem("Open in Player");
        openInPlayer.setOnAction(e -> openInPlayer(selectedModels.get(0)));
        MenuItem follow = new MenuItem("Follow");
        follow.setOnAction(e -> follow(selectedModels));
        follow.setDisable(!selectedModels.stream().allMatch(m -> m.getSite().supportsFollow() && m.getSite().credentialsAvailable()));
        MenuItem ignore = new MenuItem("Ignore");
        ignore.setOnAction(e -> ignore(selectedModels));
        MenuItem notes = new MenuItem("Notes");
        notes.setOnAction(e -> notes(selectedModels));

        ContextMenu menu = new CustomMouseBehaviorContextMenu(start, stop, copyUrl, openInPlayer, openInBrowser, follow, notes, ignore);

        if (selectedModels.size() > 1) {
            copyUrl.setDisable(true);
            openInPlayer.setDisable(true);
            openInBrowser.setDisable(true);
            notes.setDisable(true);
        }

        return menu;
    }

    private void ignore(ObservableList<JavaFxModel> selectedModels) {
        new IgnoreModelsAction(table, selectedModels, recorder, true).execute();
    }

    private void follow(ObservableList<JavaFxModel> selectedModels) {
        new FollowAction(getTabPane(), new ArrayList<>(selectedModels)).execute();
    }

    private void notes(ObservableList<JavaFxModel> selectedModels) {
        new EditNotesAction(getTabPane(), selectedModels.get(0), table).execute();
    }

    private void openInPlayer(JavaFxModel selectedModel) {
        new PlayAction(getTabPane(), selectedModel).execute();
    }

    private void startAction(List<JavaFxModel> selectedModels) {
        List<Model> models = selectedModels.stream().map(JavaFxModel::getDelegate).collect(Collectors.toList());
        new ResumeAction(table, models, recorder).execute();
    }

    private void stopAction(List<JavaFxModel> selectedModels) {
        boolean confirmed = true;
        if (Config.getInstance().getSettings().confirmationForDangerousActions) {
            int n = selectedModels.size();
            String plural = n > 1 ? "s" : "";
            String header = "This will remove " + n + " model" + plural;
            confirmed = Dialogs.showConfirmDialog("Remove From List", "Continue?", header, table.getScene());
        }
        if (confirmed) {
            List<Model> models = selectedModels.stream().map(JavaFxModel::getDelegate).collect(Collectors.toList());
            new StopRecordingAction(getTabPane(), models, recorder).execute(m -> Platform.runLater(() -> {
                table.getSelectionModel().clearSelection(table.getItems().indexOf(m));
                table.getItems().remove(m);
            }));
        }
    }

    public void saveState() {
        if (!table.getSortOrder().isEmpty()) {
            TableColumn<JavaFxModel, ?> col = table.getSortOrder().get(0);
            Config.getInstance().getSettings().recordLaterSortColumn = col.getText();
            Config.getInstance().getSettings().recordLaterSortType = 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().recordLaterColumnWidths = columnWidths;
        Config.getInstance().getSettings().recordLaterColumnIds = columnIds;
    }

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

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

    private void restoreColumnOrder() {
        String[] columnIds = Config.getInstance().getSettings().recordLaterColumnIds;
        ObservableList<TableColumn<JavaFxModel,?>> 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<JavaFxModel, ?> col = columns.get(j);
                    columns.remove(j); // NOSONAR
                    columns.add(i, col);
                }
            }
        }
    }

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

    private class ClickableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
        @Override
        public TableCell<S, T> call(TableColumn<S, T> param) {
            TableCell<S, T> cell = new TableCell<>() {
                @Override
                protected void updateItem(Object item, boolean empty) {
                    setText(empty ? "" : Objects.toString(item));
                }
            };

            cell.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
                if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
                    JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem();
                    if (selectedModel != null) {
                        new PlayAction(table, selectedModel).execute();
                    }
                }
            });
            return cell;
        }
    }
}
