package de.renew.navigator;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import javax.swing.SwingWorker;

import CH.ifa.draw.IOHelper;
import de.renew.ioontology.FileFilter;
import de.renew.navigator.io.FileFilterBuilder;
import de.renew.navigator.io.FilesystemIOLoader;
import de.renew.navigator.io.IOLoader;
import de.renew.navigator.io.ProgressListener;
import de.renew.navigator.models.BackgroundTask;
import de.renew.navigator.models.TreeElement;
import de.renew.plugin.IPlugin;
import de.renew.plugin.PluginManager;
import de.renew.plugin.PluginProperties;
import de.renew.util.PathEntry;
import de.renew.util.StringUtil;


/**
 * @author Konstantin Simon Maria Moellers
 * @version 2015-08-25
 */
public class FilesystemController extends NavigatorController {
    /**
     * Logger instance used for logging messages related to the operations
     * and activities of the {@code FilesystemController} class. This logger
     * is configured to capture and output relevant information, warnings,
     * and errors within the context of the filesystem control and handling
     * operations performed by the class.
     */
    public static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(FilesystemController.class);
    /**
     * This constant is a predefined prefix for all Navigator plugin properties.
     */
    public static final String NAVIGATOR_PREFIX = "de.renew.navigator";
    /**
     * Represents the location of files that should be loaded when the Navigator starts.
     * This constant is a combination of a predefined prefix and the specific '.filesAtStartup'
     * identifier.
     */
    public static final String FILES_AT_STARTUP = NAVIGATOR_PREFIX + ".filesAtStartup";
    /**
     * Represents the location of the workspace directory used by the Navigator.
     * This constant is a combination of a predefined prefix and the specific
     * '.workspace' identifier.
     */
    public static final String WORKSPACE_LOCATION = NAVIGATOR_PREFIX + ".workspace";
    /**
     * Represents the location of the net path used by the navigator.
     */
    public static final String NET_PATH = "de.renew.netPath";
    private final IOLoader _ioLoader;
    private final de.renew.ioontology.FileFilterBuilder _fileFilterBuilder;
    private final HashSet<SwingWorker<TreeElement, Void>> _activeWorkers;

    /**
     * @param plugin the plugin containing the controller
     * @param fileFilterBuilder the builder for the file filter
     * @deprecated This constructor is not to be used anymore.
     * Please use the constructor {@link FilesystemController#FilesystemController(NavigatorPlugin, de.renew.ioontology.FileFilterBuilder)} instead.
     */
    @Deprecated(since = "5.0", forRemoval = true)
    public FilesystemController(NavigatorPlugin plugin, FileFilterBuilder fileFilterBuilder) {
        this(plugin, new de.renew.ioontology.FileFilterBuilder() {
            @Override
            public FileFilter buildFileFilter() {
                return fileFilterBuilder.buildFileFilter();
            }

            @Override
            public boolean isExternallyOpenedFile(File file) {
                return fileFilterBuilder.isExternallyOpenedFile(file);
            }
        });
    }

    /**
     * Creates a new file system controller.
     * @param plugin the plugin containing the controller
     * @param fileFilterBuilder the builder for the file filter
     */
    public FilesystemController(
        NavigatorPlugin plugin, de.renew.ioontology.FileFilterBuilder fileFilterBuilder)
    {
        super(plugin);
        this._fileFilterBuilder = fileFilterBuilder;
        this._ioLoader = new FilesystemIOLoader(fileFilterBuilder);
        _activeWorkers = new HashSet<>();
    }

    /**
     * Finds out, if a file should be opened externally.
     *
     * @param file the file to check
     * @return <code>true</code>, if the file should be opened externally
     */
    public boolean isExternallyOpenedFile(File file) {
        return _fileFilterBuilder.isExternallyOpenedFile(file);
    }

    /**
     * Loads a root directory into a model by giving a filesystem directory.
     *
     * @param rootDirectories filesystem directories to scan
     */
    public void loadRootDirectories(Collection<File> rootDirectories) {
        for (final File rootDir : rootDirectories) {
            addLoadingTask(new LoadingTask() {
                @Override
                public String getName() {
                    return String.format("Loading %s ...", rootDir.getName());
                }

                @Override
                public TreeElement performAction(ProgressListener listener) {
                    return _ioLoader.loadPath(rootDir, listener);
                }
            });
        }
    }

    /**
     * This method loads predefined directories from plugin properties,
     * filtered by the CombinationFileFilter from CH.ifa.draw.IOHelper.
     * Each file and directory gets added by the addFile()
     * method. The retrieved CombinationFileFilter will be applied to each of these
     * files.
     */
    public void loadFromProperties() {
        cancelAllLoadingTasks();
        _model.clear();

        // Load paths from properties
        final PluginProperties properties = _plugin.getProperties();
        final String systemProperty = System.getProperty("user.dir");
        final String workspaceLocation = properties.getProperty(WORKSPACE_LOCATION, systemProperty);
        File workspaceDir = new File(workspaceLocation);
        try {
            // make relative path absolute
            workspaceDir = workspaceDir.getCanonicalFile();
        } catch (IOException e) {
            workspaceDir = new File(systemProperty);
        }
        final String filesAtStartup = properties.getProperty(FILES_AT_STARTUP, ".");

        // Collect all directories to add.
        LinkedList<File> files = getFiles(filesAtStartup, workspaceDir);

        loadRootDirectories(files);
    }

    private static LinkedList<File> getFiles(String filesAtStartup, File workspaceDir) {
        LinkedList<File> files = new LinkedList<>();
        for (String fileName : filesAtStartup.split(";")) {
            // Prepend workspace location if relative path.
            File dir = new File(fileName);
            if (!dir.isAbsolute()) {
                dir = new File(workspaceDir, fileName);
            }
            try {
                dir = dir.getCanonicalFile();
                if (dir.exists() && dir.isDirectory()) {
                    files.add(dir);
                }
            } catch (IOException e) {
                // do not add invalid directory
            }
        }
        return files;
    }

    /**
     * Open the NetPaths, set by simulator plugin properties.
     */
    public void loadFromNetPaths() {
        final IPlugin simulator = PluginManager.getInstance().getPluginByName("Renew Simulator");

        if (simulator == null) {
            throw new RuntimeException("Could not find Simulator plugin.");
        }

        final String property = simulator.getProperties().getProperty(NET_PATH);

        if (property == null) {
            throw new RuntimeException("Net path property is not set.");
        }

        final String[] paths = StringUtil.splitPaths(property);
        final PathEntry[] entries = StringUtil.canonizePaths(paths);

        final List<File> files = new LinkedList<>();
        for (PathEntry entry : entries) {
            if (entry.isClasspathRelative) {
                continue;
            }

            files.add(new File(entry.path));
        }

        loadRootDirectories(files);
    }

    /**
     * Takes the model and refreshes all.
     */
    public void refreshPaths() {
        cancelAllLoadingTasks();

        for (final TreeElement treeRoot : _model.getTreeRoots()) {
            addLoadingTask(new LoadingTask() {
                @Override
                public String getName() {
                    return String.format("Refreshing %s ...", treeRoot.getName());
                }

                @Override
                public TreeElement performAction(ProgressListener listener) {
                    _ioLoader.refreshPath(treeRoot, treeRoot.getFile(), listener);
                    return treeRoot;
                }
            });
        }
    }

    /**
     * Getter for building the fileFilterBuilder and returning its result.
     * @return the fileFilter of the instance.
     */
    public javax.swing.filechooser.FileFilter getFileFilter() {
        return IOHelper.mapToSwingFileFilter(_fileFilterBuilder.buildFileFilter());
    }

    /**
     * Creates a swing worker.
     * @param loadingTask executed by the method.
     */
    private void addLoadingTask(final LoadingTask loadingTask) {
        final FilesystemController ctrl = this;
        final BackgroundTask backgroundTask = new BackgroundTask(loadingTask.getName());
        final SwingWorker<TreeElement, Void> worker;

        worker = new SwingWorkerExtender(loadingTask, ctrl, backgroundTask);
        _activeWorkers.add(worker);
        worker.execute();

        // Set the cancel action on the background task.
        backgroundTask.setCancelAction(() -> {
            _activeWorkers.remove(worker);
            worker.cancel(true);
        });

        _model.addBackgroundTask(backgroundTask);
        _model.notifyObservers(this);
    }

    private final class SwingWorkerExtender extends SwingWorker<TreeElement, Void> {

        private final BackgroundTask _backgroundTask;
        private final LoadingTask _loadingTask;
        private final FilesystemController _ctrl;

        private SwingWorkerExtender(
            LoadingTask loadingTask, FilesystemController ctrl, BackgroundTask task)
        {
            this._backgroundTask = task;
            this._loadingTask = loadingTask;
            this._ctrl = ctrl;
        }

        @Override
        protected TreeElement doInBackground() throws Exception {
            // Update the background task while progressing.
            final ProgressListener listener = new ProgressListener() {
                @Override
                public void progress(float progress, int max) {
                    _backgroundTask.setCurrent(progress / max);
                    _backgroundTask.notifyObservers();
                }

                @Override
                public boolean isWorkerCancelled() {
                    return isCancelled();
                }
            };

            return _loadingTask.performAction(listener);
        }

        @Override
        protected void done() {
            _model.removeBackgroundTask(_backgroundTask);
            if (!isCancelled()) {
                _activeWorkers.remove(this);
                try {
                    final TreeElement treeElement = get();
                    if (!_model.contains(treeElement)) {
                        _model.add(treeElement);
                    }
                } catch (InterruptedException | ExecutionException e) {
                    LOGGER.error(e.getMessage(), e);
                }
            }
            _model.notifyObservers(_ctrl);
        }
    }

    private void cancelAllLoadingTasks() {
        for (Iterator<SwingWorker<TreeElement, Void>> i = _activeWorkers.iterator(); i.hasNext();) {
            SwingWorker<TreeElement, Void> worker = i.next();
            i.remove();
            worker.cancel(true);
        }
    }

    interface LoadingTask {
        String getName();

        TreeElement performAction(ProgressListener listener);
    }
}