package ctbrec.ui.tabs;

import static java.nio.charset.StandardCharsets.*;
import static java.nio.file.StandardOpenOption.*;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

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

import com.google.common.eventbus.Subscribe;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;

import ctbrec.Config;
import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.event.EventBusHolder;
import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.ShutdownListener;
import ctbrec.ui.action.FollowAction;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.StartRecordingAction;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.DateTimeCellFactory;
import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.event.PlayerStartedEvent;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
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.input.Clipboard;
import javafx.scene.input.ClipboardContent;
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.HBox;
import javafx.scene.layout.Priority;
import javafx.util.Callback;

public class RecentlyWatchedTab extends Tab implements ShutdownListener {

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

    private ObservableList<PlayerStartedEvent> filteredModels = FXCollections.observableArrayList();
    private ObservableList<PlayerStartedEvent> observableModels = FXCollections.observableArrayList();
    private TableView<PlayerStartedEvent> table = new TableView<>();
    private ContextMenu popup;
    private ReentrantLock lock = new ReentrantLock();
    private Recorder recorder;
    private List<Site> sites;

    public RecentlyWatchedTab(Recorder recorder, List<Site> sites) {
        this.recorder = recorder;
        this.sites = sites;
        setText("Recently Watched");
        createGui();
        loadHistory();
        subscribeToPlayerEvents();
        setOnClosed(evt -> onShutdown());
    }

    private void createGui() {
        BorderPane layout = new BorderPane();
        layout.setPadding(new Insets(5, 10, 10, 10));

        SearchBox filterInput = new SearchBox(false);
        filterInput.setPromptText("Filter");
        filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> {
            String filter = filterInput.getText();
            lock.lock();
            try {
                filter(filter);
            } finally {
                lock.unlock();
            }
        });
        filterInput.getStyleClass().remove("search-box-icon");
        HBox.setHgrow(filterInput, Priority.ALWAYS);
        HBox topBar = new HBox(5);
        topBar.getChildren().addAll(filterInput);
        layout.setTop(topBar);
        BorderPane.setMargin(topBar, new Insets(0, 0, 5, 0));

        table.setItems(observableModels);
        table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        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<PlayerStartedEvent> selectedModels = table.getSelectionModel().getSelectedItems();
            if (event.getCode() == KeyCode.DELETE) {
                delete(selectedModels);
            }
        });

        int idx = 0;
        TableColumn<PlayerStartedEvent, String> name = createTableColumn("Model", 200, idx++);
        name.setId("name");
        name.setCellValueFactory(cdf -> new SimpleStringProperty(cdf.getValue().getModel().getDisplayName()));
        name.setCellFactory(new ClickableCellFactory<>());
        table.getColumns().add(name);

        TableColumn<PlayerStartedEvent, String> url = createTableColumn("URL", 400, idx);
        url.setCellValueFactory(cdf -> new SimpleStringProperty(cdf.getValue().getModel().getUrl()));
        url.setCellFactory(new ClickableCellFactory<>());
        url.setEditable(false);
        url.setId("url");
        table.getColumns().add(url);

        TableColumn<PlayerStartedEvent, Instant> timestamp = createTableColumn("Timestamp", 150, idx);
        timestamp.setId("timestamp");
        timestamp.setCellValueFactory(cdf -> new SimpleObjectProperty<Instant>(cdf.getValue().getTimestamp()));
        timestamp.setCellFactory(new DateTimeCellFactory<>());
        timestamp.setEditable(false);
        timestamp.setSortType(SortType.DESCENDING);
        table.getColumns().add(timestamp);
        table.getSortOrder().add(timestamp);

        ScrollPane scrollPane = new ScrollPane();
        scrollPane.setFitToHeight(true);
        scrollPane.setFitToWidth(true);
        scrollPane.setContent(table);
        scrollPane.setStyle("-fx-background-color: -fx-background");
        layout.setCenter(scrollPane);
        setContent(layout);
    }

    private <T> TableColumn<PlayerStartedEvent, T> createTableColumn(String text, int width, int idx) {
        TableColumn<PlayerStartedEvent, T> tc = new TableColumn<>(text);
        tc.setPrefWidth(width);
        tc.setUserData(idx);
        return tc;
    }

    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<PlayerStartedEvent, ?> 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) {
                    PlayerStartedEvent sessionState = table.getItems().get(i);
                    filteredModels.add(sessionState);
                }
            }
            observableModels.removeAll(filteredModels);
        } finally {
            lock.unlock();
        }
    }

    private ContextMenu createContextMenu() {
        ObservableList<PlayerStartedEvent> selectedRows = table.getSelectionModel().getSelectedItems();
        if (selectedRows.isEmpty()) {
            return null;
        }

        List<Model> selectedModels = selectedRows.stream().map(PlayerStartedEvent::getModel).collect(Collectors.toList());
        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 startRecording = new MenuItem("Start Recording");
        startRecording.setOnAction(e -> startRecording(selectedModels));
        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 -> new FollowAction(getTabPane(), selectedModels).execute());
        MenuItem delete = new MenuItem("Delete");
        delete.setOnAction(e -> delete(selectedRows));

        MenuItem clearHistory = new MenuItem("Clear history");
        clearHistory.setOnAction(e -> clearHistory());

        ContextMenu menu = new CustomMouseBehaviorContextMenu();
        menu.getItems().addAll(startRecording, copyUrl, openInPlayer, openInBrowser, follow, delete, clearHistory);

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

        return menu;
    }

    private void clearHistory() {
        observableModels.clear();
        filteredModels.clear();
    }

    private void delete(List<PlayerStartedEvent> selectedRows) {
        observableModels.removeAll(selectedRows);
    }

    private void startRecording(List<Model> selectedModels) {
        new StartRecordingAction(getTabPane(), selectedModels, recorder).execute();
    }

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

    private void subscribeToPlayerEvents() {
        EventBusHolder.BUS.register(new Object() {
            @Subscribe
            public void handleEvent(PlayerStartedEvent evt) {
                table.getItems().add(evt);
                table.sort();
            }
        });
    }

    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) {
                    Model selectedModel = table.getSelectionModel().getSelectedItem().getModel();
                    if(selectedModel != null) {
                        new PlayAction(table, selectedModel).execute();
                    }
                }
            });
            return cell;
        }
    }

    private void saveHistory() throws IOException {
        Moshi moshi = new Moshi.Builder()
                .add(Model.class, new ModelJsonAdapter(sites))
                .add(Instant.class, new InstantJsonAdapter())
                .build();
        Type type = Types.newParameterizedType(List.class, PlayerStartedEvent.class);
        JsonAdapter<List<PlayerStartedEvent>> adapter = moshi.adapter(type);
        String json = adapter.indent("  ").toJson(observableModels);
        File recentlyWatched = new File(Config.getInstance().getConfigDir(), "recently_watched.json");
        LOG.debug("Saving recently watched models to {}", recentlyWatched.getAbsolutePath());
        Files.createDirectories(recentlyWatched.getParentFile().toPath());
        Files.write(recentlyWatched.toPath(), json.getBytes(UTF_8), CREATE, WRITE, TRUNCATE_EXISTING);
    }

    private void loadHistory() {
        File recentlyWatched = new File(Config.getInstance().getConfigDir(), "recently_watched.json");
        if(!recentlyWatched.exists()) {
            return;
        }

        LOG.debug("Loading recently watched models from {}", recentlyWatched.getAbsolutePath());
        Moshi moshi = new Moshi.Builder()
                .add(Model.class, new ModelJsonAdapter(sites))
                .add(Instant.class, new InstantJsonAdapter())
                .build();
        Type type = Types.newParameterizedType(List.class, PlayerStartedEvent.class);
        JsonAdapter<List<PlayerStartedEvent>> adapter = moshi.adapter(type);
        try {
            List<PlayerStartedEvent> fromJson = adapter.fromJson(Files.readString(recentlyWatched.toPath(), UTF_8));
            observableModels.addAll(fromJson);
        } catch (IOException e) {
            LOG.error("Couldn't load recently watched models", e);
        }
    }

    @Override
    public void onShutdown() {
        try {
            saveHistory();
        } catch (IOException e) {
            LOG.error("Couldn't safe recently watched models", e);
        }
    }
}
