package uk.yermak.audiobookconverter.fx;

import com.google.common.collect.ImmutableSet;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.geometry.Side;
import javafx.scene.control.*;
import javafx.scene.input.TransferMode;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import javafx.stage.Window;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOCase;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.yermak.audiobookconverter.*;

import java.io.File;
import java.lang.invoke.MethodHandles;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;

/**
 * Created by Yermak on 04-Feb-18.
 */
public class FilesController {
    final static Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

    @FXML
    private ComboBox<String> splitFile;

    @FXML
    private Button addButton;
    @FXML
    private Button removeButton;
    @FXML
    private Button clearButton;
    @FXML
    private Button upButton;
    @FXML
    private Button downButton;

    @FXML
    private Button importButton;

    @FXML
    private TabPane filesChapters;
    @FXML
    private Tab chaptersTab;
    @FXML
    private Tab filesTab;

    @FXML
    private Tab queueTab;


    @FXML
    private ListView<ProgressComponent> progressQueue;

    @FXML
    private TabPane tabs;


    @FXML
    private Button pauseButton;
    @FXML
    private Button stopButton;

    @FXML
    private ListView<MediaInfo> fileList;

    @FXML
    TreeTableView<Organisable> bookStructure;
    @FXML
    private TreeTableColumn<Organisable, String> chapterColumn;
    @FXML
    private TreeTableColumn<Organisable, String> durationColumn;
    @FXML
    private TreeTableColumn<Organisable, String> detailsColumn;

    @FXML
    private Button startButton;


    private static final String M4B = "m4b";
    private static final String M4A = "m4a";
    public static final String MP3 = "mp3";
    public static final String WMA = "wma";
    public static final String FLAC = "flac";
    public static final String AAC = "aac";
    public static final String OGG = "ogg";
    private final static String[] FILE_EXTENSIONS = {MP3, M4A, M4B, WMA, FLAC, OGG, AAC};

    private final ContextMenu contextMenu = new ContextMenu();

    private final BooleanProperty chaptersMode = new SimpleBooleanProperty(false);
    private boolean split;

    @FXML
    public void initialize() {
        ConversionContext context = ConverterApplication.getContext();

        addDragEvenHandlers(bookStructure);
        addDragEvenHandlers(fileList);
        addDragEvenHandlers(progressQueue);

        fileList.getSelectionModel().getSelectedItems().addListener((ListChangeListener<MediaInfo>) c -> {
            ConverterApplication.getContext().getSelectedMedia().clear();
            ConverterApplication.getContext().getSelectedMedia().addAll(c.getList());
        });

        splitFile.getSelectionModel().select(0);
        splitFile.getSelectionModel().selectedItemProperty().addListener((observableValue, oldValue, newValue) -> {
            switch (newValue) {
                case "parts" -> split = false;
                case "chapters" -> split = true;
            }
        });

//        fileList.setCellFactory(new ListViewListCellCallback());
        MenuItem item1 = new MenuItem("Files");
        item1.setOnAction(e -> selectFilesDialog());
        MenuItem item2 = new MenuItem("Folder");
        item2.setOnAction(e -> selectFolderDialog());
        contextMenu.getItems().addAll(item1, item2);

        ObservableList<MediaInfo> media = context.getMedia();
        fileList.setItems(media);
        fileList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);


        ObservableList<MediaInfo> selectedMedia = context.getSelectedMedia();

        selectedMedia.addListener((InvalidationListener) observable -> {
            if (selectedMedia.isEmpty()) return;
            if (!chaptersMode.get()) {
                List<MediaInfo> change = new ArrayList<>(selectedMedia);
                List<MediaInfo> selection = new ArrayList<>(fileList.getSelectionModel().getSelectedItems());
                if (!change.containsAll(selection) || !selection.containsAll(change)) {
                    fileList.getSelectionModel().clearSelection();
                    change.forEach(m -> fileList.getSelectionModel().select(media.indexOf(m)));
                }
            }
        });

        filesChapters.getTabs().remove(filesTab);
        filesChapters.getTabs().remove(chaptersTab);

        bookStructure.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

        chapterColumn.setCellValueFactory(p -> new ReadOnlyObjectWrapper<>(p.getValue().getValue().getTitle()));
        detailsColumn.setCellValueFactory(p -> new ReadOnlyObjectWrapper<>(p.getValue().getValue().getDetails()));
        durationColumn.setCellValueFactory(p -> new ReadOnlyObjectWrapper<>(Utils.formatTime(p.getValue().getValue().getDuration())));


        importButton.setDisable(true);

        chaptersMode.addListener((observableValue, oldValue, newValue) -> importButton.setDisable(newValue || fileList.getItems().isEmpty()));
        fileList.getItems().addListener((ListChangeListener<MediaInfo>) change -> importButton.setDisable(fileList.getItems().isEmpty()));

        fileList.getItems().addListener((ListChangeListener<MediaInfo>) change -> {
                    if (fileList.getItems().isEmpty()) {
                        filesChapters.getTabs().remove(filesTab);
                    }
                }
        );

    }

    private void addDragEvenHandlers(Control control) {
        control.setOnDragOver(event -> {
            if (event.getGestureSource() != control && event.getDragboard().hasFiles()) {
                event.acceptTransferModes(TransferMode.ANY);
            }

            event.consume();
        });

        control.setOnDragDropped(event -> {
            processFiles(event.getDragboard().getFiles());
            event.setDropCompleted(true);
            event.consume();
            if (!chaptersMode.get()) {
                if (!filesChapters.getTabs().contains(filesTab)) {
                    filesChapters.getTabs().add(filesTab);
                    filesChapters.getSelectionModel().select(filesTab);
                }
            }
        });
    }


    @FXML
    protected void addFiles(ActionEvent event) {
        Button node = (Button) event.getSource();
        contextMenu.show(node, Side.RIGHT, 0, 0);
    }

    public void selectFolderDialog() {
        Window window = ConverterApplication.getEnv().getWindow();
        DirectoryChooser directoryChooser = new DirectoryChooser();
        String sourceFolder = AppProperties.getProperty("source.folder");
        directoryChooser.setInitialDirectory(Utils.getInitialDirecotory(sourceFolder));

        StringJoiner filetypes = new StringJoiner("/");

        Arrays.stream(FILE_EXTENSIONS).map(String::toUpperCase).forEach(filetypes::add);

        directoryChooser.setTitle("Select folder with " + filetypes.toString() + " files for conversion");
        File selectedDirectory = directoryChooser.showDialog(window);
        if (selectedDirectory != null) {
            processFiles(Collections.singleton(selectedDirectory));
            AppProperties.setProperty("source.folder", selectedDirectory.getAbsolutePath());
            if (!chaptersMode.get()) {
                filesChapters.getTabs().add(filesTab);
                filesChapters.getSelectionModel().select(filesTab);
            }
        }
    }


    private void processFiles(Collection<File> files) {
        List<String> fileNames = collectFiles(files);

        List<MediaInfo> addedMedia = createMediaLoader(fileNames).loadMediaInfo();
        if (chaptersMode.get()) {
            Book book = ConverterApplication.getContext().getBook();
            book.construct(FXCollections.observableArrayList(addedMedia));
            updateBookStructure(book, bookStructure.getRoot());
        } else {
            fileList.getItems().addAll(addedMedia);
        }
    }

    private static String[] toSuffixes(String prefix, final String[] extensions) {
        final String[] suffixes = new String[extensions.length];
        for (int i = 0; i < extensions.length; i++) {
            suffixes[i] = prefix + extensions[i];
        }
        return suffixes;
    }

    private List<String> collectFiles(Collection<File> files) {
        List<String> fileNames = new ArrayList<>();
        ImmutableSet<String> extensions = ImmutableSet.copyOf(FILE_EXTENSIONS);

        for (File file : files) {
            if (file.isDirectory()) {
                SuffixFileFilter suffixFileFilter = new SuffixFileFilter(toSuffixes(".", FILE_EXTENSIONS), IOCase.INSENSITIVE);
                Collection<File> nestedFiles = FileUtils.listFiles(file, suffixFileFilter, TrueFileFilter.INSTANCE);
                nestedFiles.stream().map(File::getPath).forEach(fileNames::add);
            } else {
                boolean allowedFileExtension = extensions.contains(FilenameUtils.getExtension(file.getName()).toLowerCase());
                if (allowedFileExtension) {
                    fileNames.add(file.getPath());
                }
            }
        }
        return fileNames;
    }

    private FFMediaLoader createMediaLoader(List<String> fileNames) {
        return new FFMediaLoader(fileNames, ConverterApplication.getContext().getPlannedConversionGroup());
    }

    public void selectFilesDialog() {
        Window window = ConverterApplication.getEnv().getWindow();
        final FileChooser fileChooser = new FileChooser();
        String sourceFolder = AppProperties.getProperty("source.folder");
        fileChooser.setInitialDirectory(Utils.getInitialDirecotory(sourceFolder));
        StringJoiner filetypes = new StringJoiner("/");

        Arrays.stream(FILE_EXTENSIONS).map(String::toUpperCase).forEach(filetypes::add);

        fileChooser.setTitle("Select " + filetypes.toString() + " files for conversion");

        fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Audio", Arrays.asList(toSuffixes("*.", FILE_EXTENSIONS))));


        List<File> files = fileChooser.showOpenMultipleDialog(window);
        if (files != null) {
            processFiles(files);
            File firstFile = files.get(0);
            File parentFile = firstFile.getParentFile();
            AppProperties.setProperty("source.folder", parentFile.getAbsolutePath());
            if (!chaptersMode.get()) {
                filesChapters.getTabs().add(filesTab);
                filesChapters.getSelectionModel().select(filesTab);
            }
        }
    }

    public void removeFiles(ActionEvent event) {
        if (chaptersMode.get()) {
            ObservableList<TreeTablePosition<Organisable, ?>> selectedCells = bookStructure.getSelectionModel().getSelectedCells();
//            int min = bookStructure.getExpandedItemCount();
            for (TreeTablePosition<Organisable, ?> selectedCell : selectedCells) {
                Organisable organisable = selectedCell.getTreeItem().getValue();
                organisable.remove();
//                min = Math.min(selectedCell.getRow(), min - 1);
            }


//            int finalMin = min;
            Platform.runLater(() -> {
                updateBookStructure(ConverterApplication.getContext().getBook(), bookStructure.getRoot());
                //TODO temp hack - can't selection on previous row causes NPE on next removal...
                bookStructure.getSelectionModel().clearSelection();
                if (bookStructure.getRoot().getChildren().isEmpty()) {
                    clear(event);
                }
            });

        } else {
            ObservableList<MediaInfo> selected = fileList.getSelectionModel().getSelectedItems();
            fileList.getItems().removeAll(selected);
        }
    }

    public void clear(ActionEvent event) {
        fileList.getItems().clear();
        ConverterApplication.getContext().getPlannedConversionGroup().cancel();
        ConverterApplication.getContext().resetForNewConversion();
        bookStructure.setRoot(null);
        filesChapters.getTabs().remove(filesTab);
        filesChapters.getTabs().remove(chaptersTab);
        chaptersMode.set(false);
    }

    public void moveUp(ActionEvent event) {
        if (chaptersMode.get()) {
            ObservableList<TreeTablePosition<Organisable, ?>> selectedCells = bookStructure.getSelectionModel().getSelectedCells();
            if (selectedCells.size() == 1) {
                Organisable organisable = selectedCells.get(0).getTreeItem().getValue();
                organisable.moveUp();
                Platform.runLater(() -> updateBookStructure(ConverterApplication.getContext().getBook(), bookStructure.getRoot()));
            }
        } else {
            ObservableList<Integer> selectedIndices = fileList.getSelectionModel().getSelectedIndices();
            if (selectedIndices.size() == 1) {
                ObservableList<MediaInfo> items = fileList.getItems();
                int selected = selectedIndices.get(0);
                if (selected > 0) {
                    MediaInfo upper = items.get(selected - 1);
                    MediaInfo lower = items.get(selected);
                    items.set(selected - 1, lower);
                    items.set(selected, upper);
                    fileList.getSelectionModel().clearAndSelect(selected - 1);
                }
            }
        }
    }

    public void moveDown(ActionEvent event) {
        if (chaptersMode.get()) {
            ObservableList<TreeTablePosition<Organisable, ?>> selectedCells = bookStructure.getSelectionModel().getSelectedCells();
            if (selectedCells.size() == 1) {
                Organisable organisable = selectedCells.get(0).getTreeItem().getValue();
                organisable.moveDown();
                Platform.runLater(() -> updateBookStructure(ConverterApplication.getContext().getBook(), bookStructure.getRoot()));
            }
        } else {
            ObservableList<Integer> selectedIndices = fileList.getSelectionModel().getSelectedIndices();
            if (selectedIndices.size() == 1) {
                ObservableList<MediaInfo> items = fileList.getItems();
                int selected = selectedIndices.get(0);
                if (selected < items.size() - 1) {
                    MediaInfo lower = items.get(selected + 1);
                    MediaInfo upper = items.get(selected);
                    items.set(selected, lower);
                    items.set(selected + 1, upper);
                    fileList.getSelectionModel().clearAndSelect(selected + 1);
                }
            }
        }
    }

    private void launch(ConversionGroup conversionGroup, Book book, ObservableList<MediaInfo> mediaInfos, ProgressComponent progressComponent, String outputDestination) {

        if (book == null) {
            book = new Book(ConverterApplication.getContext().getBookInfo().get());
            book.construct(mediaInfos);
        }

        ObservableList<Part> parts = book.getParts();
        String extension = FilenameUtils.getExtension(outputDestination);
        conversionGroup.getOutputParameters().setupFormat(extension);

        if (split) {
            List<Chapter> chapters = parts.stream().flatMap(p -> p.getChapters().stream()).collect(Collectors.toList());
            logger.debug("Found {} chapters in the book", chapters.size());
            for (int i = 0; i < chapters.size(); i++) {
                Chapter chapter = chapters.get(i);
                String finalDesination = outputDestination;
                if (chapters.size() > 1) {
                    finalDesination = finalDesination.replace("." + extension, ", Chapter " + (i + 1) + "." + extension);
                }
                String finalName = new File(finalDesination).getName();
                logger.debug("Adding conversion for chapter {}", finalName);

                ConversionProgress conversionProgress = conversionGroup.start(chapter, finalDesination);
                Platform.runLater(() -> {
                    progressQueue.getItems().add(0, new ProgressComponent(conversionProgress));
                });

            }
        } else {
            logger.debug("Found {} parts in the book", parts.size());
            for (int i = 0; i < parts.size(); i++) {
                Part part = parts.get(i);
                String finalDesination = outputDestination;
                if (parts.size() > 1) {
                    finalDesination = finalDesination.replace("." + extension, ", Part " + (i + 1) + "." + extension);
                }
                String finalName = new File(finalDesination).getName();
                logger.debug("Adding conversion for part {}", finalName);

                ConversionProgress conversionProgress = conversionGroup.start(part, finalDesination);
                Platform.runLater(() -> {
                    progressQueue.getItems().add(0, new ProgressComponent(conversionProgress));
                });
            }
        }

        Platform.runLater(() -> progressQueue.getItems().remove(progressComponent));
    }

    public synchronized void start(ActionEvent actionEvent) {
        ConversionContext context = ConverterApplication.getContext();
        if (context.getBook() == null && fileList.getItems().isEmpty()) return;

        String outputDestination = selectOutputFile(ConverterApplication.getContext().getBookInfo().get());

        if (outputDestination == null) {
            return;
        }

        ObservableList<MediaInfo> mediaInfos = FXCollections.observableArrayList(fileList.getItems());

        ProgressComponent placeHolderProgress = new ProgressComponent(new ConversionProgress(new ConversionJob(context.getPlannedConversionGroup(), Convertable.EMPTY, Collections.emptyMap(), outputDestination)));

        ConversionGroup conversionGroup = ConverterApplication.getContext().getPlannedConversionGroup();

        conversionGroup.setOutputParameters(context.getOutputParameters());
        conversionGroup.setBookInfo(context.getBookInfo().get());
        conversionGroup.setPosters(new ArrayList<>(context.getPosters()));

        Executors.newSingleThreadExecutor().submit(() -> {
            Platform.runLater(() -> {
                progressQueue.getItems().add(0, placeHolderProgress);
                filesChapters.getSelectionModel().select(queueTab);
            });
            launch(conversionGroup, context.getBook(), mediaInfos, placeHolderProgress, outputDestination);
        });

        ConverterApplication.getContext().resetForNewConversion();
        bookStructure.setRoot(null);
        filesChapters.getTabs().remove(filesTab);
        filesChapters.getTabs().remove(chaptersTab);
        fileList.getItems().clear();
        chaptersMode.set(false);
    }

    private static String selectOutputFile(AudioBookInfo audioBookInfo) {
        JfxEnv env = ConverterApplication.getEnv();

        final FileChooser fileChooser = new FileChooser();
        String outputFolder = AppProperties.getProperty("output.folder");
        fileChooser.setInitialDirectory(Utils.getInitialDirecotory(outputFolder));
        fileChooser.setInitialFileName(Utils.getOuputFilenameSuggestion(audioBookInfo));
        fileChooser.setTitle("Save AudioBook");
        fileChooser.getExtensionFilters().addAll(
                new FileChooser.ExtensionFilter(M4B, "*." + M4B),
                new FileChooser.ExtensionFilter(M4A, "*." + M4A),
                new FileChooser.ExtensionFilter(MP3, "*." + MP3),
                new FileChooser.ExtensionFilter(OGG, "*." + OGG)
        );
        File file = fileChooser.showSaveDialog(env.getWindow());
        if (file == null) return null;
        File parentFolder = file.getParentFile();
        AppProperties.setProperty("output.folder", parentFolder.getAbsolutePath());
        return file.getPath();
    }


    public synchronized void importChapters(ActionEvent actionEvent) {
        if (fileList.getItems().isEmpty()) {
            return;
        }

        startButton.setDisable(true);

        filesChapters.getTabs().add(chaptersTab);
        filesChapters.getTabs().remove(filesTab);

        bookStructure.setShowRoot(false);

        ObservableList<MediaInfo> mediaInfos = FXCollections.observableArrayList(fileList.getItems());

        Book book = new Book(ConverterApplication.getContext().getBookInfo().get());

        TreeItem<Organisable> bookItem = new TreeItem<>(book);
        bookStructure.setRoot(bookItem);

        updateBookStructure(book, bookItem);

        bookItem.setExpanded(true);
        ConverterApplication.getContext().setBook(book);
        filesChapters.getSelectionModel().select(chaptersTab);
        fileList.getItems().clear();
        chaptersMode.set(true);


        long lastBookUpdate = System.currentTimeMillis();
        book.addListener(observable -> {
            logger.debug("Captured book modification");
            if (System.currentTimeMillis() - lastBookUpdate > 1000) {
                Platform.runLater(() -> updateBookStructure(book, bookItem));
            }
        });

        Executors.newSingleThreadExecutor().submit(() -> {
            try {
                book.construct(mediaInfos);
                updateBookStructure(book, bookItem);
            } finally {
                startButton.setDisable(false);
            }
        });
    }

    private void updateBookStructure(Book book, TreeItem<Organisable> bookItem) {
        bookStructure.getRoot().getChildren().clear();
        book.getParts().forEach(p -> {
            TreeItem<Organisable> partItem = new TreeItem<>(p);
            bookItem.getChildren().add(partItem);
            p.getChapters().forEach(c -> {
                TreeItem<Organisable> chapterItem = new TreeItem<>(c);
                partItem.getChildren().add(chapterItem);
                c.getMedia().forEach(m -> chapterItem.getChildren().add(new TreeItem<>(m)));
            });
        });
        bookStructure.getRoot().getChildren().forEach(t -> t.setExpanded(true));
        bookStructure.refresh();
    }

    public void combine(ActionEvent actionEvent) {
        ObservableList<TreeTablePosition<Organisable, ?>> selectedCells = bookStructure.getSelectionModel().getSelectedCells();
        if (selectedCells.isEmpty()) return;
        List<Part> partMergers = selectedCells.stream().map(s -> s.getTreeItem().getValue()).filter(v -> (v instanceof Part)).map(c -> (Part) c).collect(Collectors.toList());
        if (partMergers.size() > 1) {
            Part recipient = partMergers.remove(0);
            recipient.combine(partMergers);
        } else {
            List<Chapter> chapterMergers = selectedCells.stream().map(s -> s.getTreeItem().getValue()).filter(v -> (v instanceof Chapter)).map(c -> (Chapter) c).collect(Collectors.toList());
            if (chapterMergers.size() > 1) {
                Chapter recipient = chapterMergers.remove(0);
                recipient.combine(chapterMergers);
            }
        }
        updateBookStructure(ConverterApplication.getContext().getBook(), bookStructure.getRoot());
    }

    public void split(ActionEvent actionEvent) {
        ObservableList<TreeTablePosition<Organisable, ?>> selectedCells = bookStructure.getSelectionModel().getSelectedCells();
        if (selectedCells.size() != 1) return;
        Organisable organisable = selectedCells.get(0).getTreeItem().getValue();
        organisable.split();
        updateBookStructure(ConverterApplication.getContext().getBook(), bookStructure.getRoot());
    }

    public void edit(ActionEvent actionEvent) {
        ObservableList<TreeTablePosition<Organisable, ?>> selectedCells = bookStructure.getSelectionModel().getSelectedCells();
        if (selectedCells.size() != 1) return;
        Organisable organisable = selectedCells.get(0).getTreeItem().getValue();
        if (organisable instanceof Chapter) {
            new ChapterEditor((Chapter) organisable).editChapter();
            bookStructure.refresh();
        }
    }


    public void pause(ActionEvent actionEvent) {
        ConversionContext context = ConverterApplication.getContext();
        if (context.isPaused()) {
            context.resumeConversions();
            pauseButton.setText("Pause all");
        } else {
            context.pauseConversions();
            pauseButton.setText("Resume all");
        }
    }

    public void stop(ActionEvent actionEvent) {
        ConverterApplication.getContext().stopConversions();
    }

    @FXML
    protected void openLink(ActionEvent event) {
        Hyperlink source = (Hyperlink) event.getSource();
        ConverterApplication.getEnv().showDocument(source.getUserData().toString());
    }

    public void openWebSite(ActionEvent actionEvent) {
        ConverterApplication.getEnv().showDocument("https://www.recoupler.com/products/audiobookconverter");
    }

    public void openAboutPage(ActionEvent actionEvent) {
        ConverterApplication.getEnv().showDocument("https://www.recoupler.com/products/audiobookconverter/about");
    }

    public void openFAQ(ActionEvent actionEvent) {
        ConverterApplication.getEnv().showDocument("https://www.recoupler.com/products/audiobookconverter/faq");
    }

    public void openDonate() {
        ConverterApplication.getEnv().showDocument("https://www.recoupler.com/products/audiobookconverter/donate");
    }

    public void checkVersion(ActionEvent actionEvent) {
        ConverterApplication.checkNewVersion();
    }

    public void exit(ActionEvent actionEvent) {
        logger.info("Closing application");
        ConverterApplication.getContext().stopConversions();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.exit(0);
    }


    public void clearQueue(ActionEvent actionEvent) {
        ObservableList<ProgressComponent> items = progressQueue.getItems();
        List<ProgressComponent> dones = new ArrayList<>();
        for (ProgressComponent item : items) {
            if (item.isOver()) dones.add(item);
        }
        Platform.runLater(() -> {
            for (ProgressComponent done : dones) {
                progressQueue.getItems().remove(done);
            }
        });
    }

    public void splitFile(ActionEvent actionEvent) {

    }

}