package photato.core;

import java.util.logging.Logger;
import java.util.logging.Level;
import photato.helpers.SearchQueryHelper;
import photato.Photato;
import photato.core.entities.PhotatoFolder;
import photato.core.entities.PhotatoPicture;
import photato.core.entities.PictureInfos;
import photato.core.metadata.IMetadataAggregator;
import photato.core.metadata.Metadata;
import photato.helpers.FileHelper;
import photato.helpers.Tuple;
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import photato.core.entities.PhotatoMedia;
import photato.core.resize.fullscreen.IFullScreenImageGetter;
import photato.core.resize.thumbnails.IThumbnailGenerator;
import photato.helpers.MediaHelper;

public class PhotatoFilesManager implements Closeable {
    private static final Logger LOGGER = Logger.getLogger( PhotatoFilesManager.class.getName() );
    private final FileSystem fileSystem;
    private final IMetadataAggregator metadataAggregator;
    private final IThumbnailGenerator thumbnailGenerator;
    private final IFullScreenImageGetter fullScreenImageGetter;
    private final PhotatoFolder rootFolder;
    private final SearchManager searchManager;
    private final AlbumsManager albumsManager;
    private final WatchServiceThread watchServiceThread;
    private final Map<Path, WatchKey> watchedDirectoriesKeys;
    private final Map<WatchKey, Path> watchedDirectoriesPaths;
    private final boolean prefixOnlyMode;
    private final boolean useParallelPicturesGeneration;

    public PhotatoFilesManager(Path rootFolder, FileSystem fileSystem, IMetadataAggregator metadataGetter, IThumbnailGenerator thumbnailGenerator, IFullScreenImageGetter fullScreenImageGetter, boolean prefixOnlyMode, boolean indexFolderName, boolean useParallelPicturesGeneration) throws IOException {
        this.fileSystem = fileSystem;
        this.metadataAggregator = metadataGetter;
        this.thumbnailGenerator = thumbnailGenerator;
        this.fullScreenImageGetter = fullScreenImageGetter;
        this.rootFolder = new PhotatoFolder(rootFolder, rootFolder);
        this.searchManager = new SearchManager(prefixOnlyMode, indexFolderName);
        this.albumsManager = new AlbumsManager();
        this.prefixOnlyMode = prefixOnlyMode;
        this.useParallelPicturesGeneration = useParallelPicturesGeneration;

        WatchService watcher = this.fileSystem.newWatchService();
        this.watchedDirectoriesKeys = new HashMap<>();
        this.watchedDirectoriesPaths = new HashMap<>();
        this.runInitialFolderExploration(watcher, this.rootFolder);
        this.watchServiceThread = new WatchServiceThread(watcher);
        this.watchServiceThread.start();
    }

    public List<PhotatoFolder> getFoldersInFolder(String folder) {
        PhotatoFolder currentFolder = isVirtualFolder(folder) ? this.albumsManager.getCurrentFolder(folder) : this.getCurrentFolder(this.rootFolder.fsPath.resolve(folder));

        if (currentFolder != null) {
            return currentFolder.subFolders.values().stream().filter((PhotatoFolder f) -> !f.isEmpty()).collect(Collectors.toList());
        } else {
            return new ArrayList<>();
        }
    }

    public List<PhotatoMedia> getMediasInFolder(String folder) {
        PhotatoFolder currentFolder = isVirtualFolder(folder) ? this.albumsManager.getCurrentFolder(folder) : this.getCurrentFolder(this.rootFolder.fsPath.resolve(folder));

        if (currentFolder != null) {
            return new ArrayList<>(currentFolder.medias);
        } else {
            return new ArrayList<>();
        }
    }

    public List<PhotatoMedia> searchMediasInFolder(String folder, String searchQuery) {
        PhotatoFolder currentFolder = isVirtualFolder(folder) ? this.albumsManager.getCurrentFolder(folder) : this.getCurrentFolder(this.rootFolder.fsPath.resolve(folder));

        return this.searchManager.searchMediasInFolder(currentFolder.fsPath, searchQuery, isVirtualFolder(folder));
    }

    public List<PhotatoFolder> searchFoldersInFolder(String folder, String searchQuery) {
        // Search for a folder with the correct name. This is just a recursive exploration since we suppose the number of folders will be low enough and thus we would be able to "bruteforce" it
        List<String> searchQuerySplit = SearchQueryHelper.getSplittedTerms(searchQuery);
        List<PhotatoFolder> result = new ArrayList<>();

        if (!searchQuerySplit.isEmpty()) {
            PhotatoFolder currentFolder = isVirtualFolder(folder) ? this.albumsManager.getCurrentFolder(folder) : this.getCurrentFolder(this.rootFolder.fsPath.resolve(folder));

            Queue<PhotatoFolder> queue = new LinkedList<>();
            queue.add(currentFolder);

            while (!queue.isEmpty()) {
                currentFolder = queue.remove();
                queue.addAll(currentFolder.subFolders.values());

                if (!currentFolder.isEmpty()) {
                    List<String> currentFolderCleanedFilename = SearchQueryHelper.getSplittedTerms(currentFolder.filename);
                    boolean ok = searchQuerySplit.stream().allMatch((s) -> (currentFolderCleanedFilename.stream().anyMatch((String t) -> (prefixOnlyMode && t.startsWith(s)) || (!prefixOnlyMode && t.contains(s)))));

                    if (ok) {
                        result.add(currentFolder);
                    }
                }
            }
        }

        return result;
    }

    @Override
    public void close() throws IOException {
        this.watchServiceThread.shutdown();
        try {
            this.watchServiceThread.join();
        } catch (InterruptedException ex) {
            throw new IOException(ex);
        }
    }

    private void runInitialFolderExploration(WatchService watcher, PhotatoFolder baseFolder) throws IOException {
        synchronized (this.rootFolder) {
            Queue<PhotatoFolder> foldersToExplore = new LinkedList<>();
            foldersToExplore.add(baseFolder);

            while (!foldersToExplore.isEmpty()) {
                PhotatoFolder currentFolder = foldersToExplore.remove();
                LOGGER.log(Level.INFO, "Exploring {0}", currentFolder);

                // Registering currentDirectory to watcher
                WatchKey key = currentFolder.fsPath.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
                this.watchedDirectoriesKeys.put(currentFolder.fsPath, key);
                this.watchedDirectoriesPaths.put(key, currentFolder.fsPath);

                List<PhotatoFolder> folders = Files.list(currentFolder.fsPath)
                        .filter((Path path) -> Files.isReadable(path) && Files.isDirectory(path) && !FileHelper.folderContainsIgnoreFile(path))
                        .map((Path path) -> new PhotatoFolder(this.rootFolder.fsPath, path))
                        .collect(Collectors.toList());
                foldersToExplore.addAll(folders);

                for (PhotatoFolder folder : folders) {
                    currentFolder.subFolders.put(folder.fsPath.getFileName().toString(), folder);
                }

                // Renaming pictures with 2+ spaces in a row since this will cause trouble then
                Files.list(currentFolder.fsPath).forEach((Path path) -> {
                    if (path.getFileName().toString().contains("  ")) {
                        String newFilename = path.getFileName().toString().replaceAll("[ ]{2,}", " ");
                        Path newPath = path.resolveSibling(newFilename);
                        path.toFile().renameTo(newPath.toFile());

                        LOGGER.log(Level.SEVERE, "Renamed {0} to {1}", new Object[] {path, newPath});
                    }
                });

                // Extraction of pictures metadata
                Map<Path, Metadata> metadatas = this.metadataAggregator.getMetadatas(
                        Files.list(currentFolder.fsPath).parallel()
                                .filter((Path path) -> MediaHelper.isPictureFile(path) || MediaHelper.isVideoFile(path))
                                .map((path) -> new Tuple<>(path, tryGetLastModifiedTimestamp(path)))
                                .collect(Collectors.toList()));

                List<PhotatoMedia> medias = metadatas.entrySet().parallelStream()
                        .map((Map.Entry<Path, Metadata> entry) -> PhotatoMedia.createMedia(this.rootFolder.fsPath, entry.getKey(), entry.getValue(),
                        new PictureInfos(this.thumbnailGenerator.getThumbnailUrl(entry.getKey(), tryGetLastModifiedTimestamp(entry.getKey())), this.thumbnailGenerator.getThumbnailWidth(entry.getValue().width, entry.getValue().height), this.thumbnailGenerator.getThumbnailHeight(entry.getValue().width, entry.getValue().height)),
                        new PictureInfos(this.fullScreenImageGetter.getImageUrl(entry.getKey(), tryGetLastModifiedTimestamp(entry.getKey())), this.fullScreenImageGetter.getImageWidth(entry.getValue().width, entry.getValue().height), this.fullScreenImageGetter.getImageHeight(entry.getValue().width, entry.getValue().height)),
                        tryGetLastModifiedTimestamp(entry.getKey())))
                        .collect(Collectors.toList());

                medias.forEach((PhotatoMedia media) -> {
                    currentFolder.medias.add(media);
                    searchManager.addMedia(rootFolder, media);
                    albumsManager.addMedia(media);
                });

                Stream<PhotatoMedia> thumbnailStream = this.useParallelPicturesGeneration ? medias.parallelStream() : medias.stream(); // This could be a parallel stream. However, since thumbnail generation takes a lot of RAM, having it parallel would take too much ram (bad on small machines)
                thumbnailStream.forEach((PhotatoMedia media) -> {
                    try {
                        thumbnailGenerator.generateThumbnail(media);
                        fullScreenImageGetter.generateImage(media);
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                });
            }
        }

        System.gc();
    }

    private PhotatoFolder getCurrentFolder(Path path) {
        synchronized (this.rootFolder) {
            Path relativePath = this.rootFolder.fsPath.relativize(path);
            String[] elmnts = relativePath.toString().replace("\\", "/").split("/");

            if (elmnts.length == 1 && elmnts[0].isEmpty()) {
                return this.rootFolder;
            }

            PhotatoFolder currentFolder = this.rootFolder;
            for (int i = 0; i < elmnts.length; i++) {
                currentFolder = currentFolder.subFolders.get(elmnts[i]);

                if (currentFolder == null) {
                    return null;
                }
            }

            return currentFolder;
        }
    }

    private class WatchServiceThread extends Thread {
        private final Logger LOGGER = Logger.getLogger( WatchServiceThread.class.getName() );
        private final WatchService watcher;
        private boolean shouldRun;

        public WatchServiceThread(WatchService watchService) {
            super("WatchServiceThread");
            this.watcher = watchService;
            this.shouldRun = true;
        }

        @Override
        public void run() {
            while (this.shouldRun) {
                try {
                    WatchKey key = this.watcher.poll(100, TimeUnit.MILLISECONDS);

                    if (key != null) {
                        for (WatchEvent event : key.pollEvents()) {
                            try {
                                WatchEvent.Kind kind = event.kind();

                                if (kind == StandardWatchEventKinds.OVERFLOW) {
                                    continue;
                                } else {
                                    synchronized (rootFolder) {
                                        WatchEvent<Path> ev = (WatchEvent<Path>) event;
                                        Path folder = watchedDirectoriesPaths.get(key);
                                        if (folder == null) {
                                            throw new Exception("Unknown watchKey!");
                                        }

                                        Path filename = folder.resolve(ev.context());
                                        LOGGER.log(Level.INFO, "Detected event: {0} on {1}", new Object[] {kind, filename});
                                        if (Files.isDirectory(filename)) {
                                            if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
                                                this.manageDirectoryCreation(filename);
                                            } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
                                                this.manageDirectoryDeletion(filename);
                                            }
                                        } else if (MediaHelper.isPictureFile(filename) || MediaHelper.isVideoFile(filename)) {
                                            if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
                                                this.manageFileCreation(filename);
                                            } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
                                                this.manageFileDeletion(filename);
                                            } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
                                                this.manageFileDeletion(filename);
                                                this.manageFileCreation(filename);
                                            }
                                        }
                                    }
                                }
                            } catch (Exception ex) {
                                ex.printStackTrace();
                            }
                        }

                        boolean valid = key.reset();
                        if (!valid) {
                            LOGGER.log(Level.SEVERE, "Not valid key, breaking");
                            // break;
                        }
                    }
                } catch (InterruptedException ex) {
                }
            }
        }

        public void shutdown() {
            this.shouldRun = false;
        }

        private void manageFileCreation(Path filename) throws IOException {
            PhotatoFolder folder = getCurrentFolder(filename.getParent());
            if (folder.medias.stream().noneMatch((PhotatoMedia p) -> p.fsPath.equals(filename))) {
                long lastModificationTimestamp = Files.getLastModifiedTime(filename).toMillis();
                Metadata metadata = metadataAggregator.getMetadata(filename, lastModificationTimestamp);
                PictureInfos thumbnailInfos = new PictureInfos(thumbnailGenerator.getThumbnailUrl(filename, lastModificationTimestamp), thumbnailGenerator.getThumbnailWidth(metadata.width, metadata.height), thumbnailGenerator.getThumbnailHeight(metadata.width, metadata.height));
                PictureInfos fullScreenInfos = new PictureInfos(fullScreenImageGetter.getImageUrl(filename, lastModificationTimestamp), fullScreenImageGetter.getImageWidth(metadata.width, metadata.height), fullScreenImageGetter.getImageHeight(metadata.width, metadata.height));
                PhotatoMedia media = PhotatoMedia.createMedia(rootFolder.fsPath, filename, metadataAggregator.getMetadata(filename, lastModificationTimestamp), thumbnailInfos, fullScreenInfos, lastModificationTimestamp);
                folder.medias.add(media);
                searchManager.addMedia(rootFolder, media);
                albumsManager.addMedia(media);
                thumbnailGenerator.generateThumbnail(media);
                fullScreenImageGetter.generateImage(media);
            }
        }

        private void manageDirectoryCreation(Path filename) throws IOException {
            PhotatoFolder newFolder = new PhotatoFolder(rootFolder.fsPath, filename);
            PhotatoFolder parentFolder = getCurrentFolder(filename.getParent());

            parentFolder.subFolders.put(filename.getFileName().toString(), newFolder);

            WatchKey key = filename.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
            watchedDirectoriesKeys.put(filename, key);
            watchedDirectoriesPaths.put(key, filename);

            runInitialFolderExploration(watcher, newFolder);
        }

        private void manageFileDeletion(Path filename) throws IOException {
            PhotatoFolder folder = getCurrentFolder(filename.getParent());
            Optional<PhotatoMedia> findAny = folder.medias.stream().filter((PhotatoMedia p) -> p.fsPath.equals(filename)).findAny();
            if (findAny.isPresent()) {
                PhotatoMedia picture = findAny.get();
                folder.medias.remove(picture);
                searchManager.removeMedia(picture);
                albumsManager.removeMedia(picture);
                thumbnailGenerator.deleteThumbnail(picture.fsPath, picture.lastModificationTimestamp);
                fullScreenImageGetter.deleteImage(picture);
            }
        }

        private void manageDirectoryDeletion(Path filename) throws IOException {
            PhotatoFolder parentFolder = getCurrentFolder(filename.getParent());
            parentFolder.subFolders.remove(filename.getFileName().toString());
            WatchKey removed = watchedDirectoriesKeys.remove(filename);
            if (removed != null) {
                removed.cancel();
                watchedDirectoriesPaths.remove(removed);
            }

            PhotatoFolder currentFolder = getCurrentFolder(filename);
            if (currentFolder.medias != null) {
                for (PhotatoMedia media : currentFolder.medias) {
                    try {
                        searchManager.removeMedia(media);
                        albumsManager.removeMedia(media);
                        thumbnailGenerator.deleteThumbnail(media.fsPath, media.lastModificationTimestamp);
                        fullScreenImageGetter.deleteImage(media);
                    } catch (IOException ex) {
                    }
                }
            }
        }

    }

    private static long tryGetLastModifiedTimestamp(Path p) {
        try {
            return Files.getLastModifiedTime(p).toMillis();
        } catch (IOException ex) {
            ex.printStackTrace();
            return 0;
        }
    }

    private static boolean isVirtualFolder(String folder) {
        return folder.startsWith(AlbumsManager.albumsVirtualRootFolderName + "/") || folder.equalsIgnoreCase(AlbumsManager.albumsVirtualRootFolderName);
    }
}