package org.ebookdroid.core.presentation;

import static android.os.FileObserver.CREATE;
import static android.os.FileObserver.DELETE;
import static android.os.FileObserver.MOVED_FROM;
import static android.os.FileObserver.MOVED_TO;

import org.ebookdroid.core.IBrowserActivity;
import org.ebookdroid.core.actions.EventDispatcher;
import org.ebookdroid.core.actions.InvokationType;
import org.ebookdroid.core.settings.SettingsManager;
import org.ebookdroid.core.utils.DirectoryFilter;
import org.ebookdroid.core.utils.FileExtensionFilter;
import org.ebookdroid.utils.LengthUtils;
import org.ebookdroid.utils.StringUtils;

import android.os.AsyncTask;
import android.os.FileObserver;

import java.io.File;
import java.io.FilenameFilter;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

public class FileSystemScanner {

    private static final int EVENT_MASK = CREATE | MOVED_TO | DELETE | MOVED_FROM;

    final IBrowserActivity base;
    final EventDispatcher listeners;
    final AtomicBoolean inScan = new AtomicBoolean();
    final Map<File, FileObserver> observers = new HashMap<File, FileObserver>();

    private ScanTask m_scanTask;

    public FileSystemScanner(final IBrowserActivity base) {
        this.base = base;
        this.listeners = new EventDispatcher(base.getActivity(), InvokationType.AsyncUI, Listener.class);
    }

    public void startScan(final String... paths) {
        if (inScan.compareAndSet(false, true)) {
            m_scanTask = new ScanTask(SettingsManager.getAppSettings().getAllowedFileTypes());
            m_scanTask.execute(paths);
        } else {
            m_scanTask.addPaths(paths);
        }
    }

    public void startScan(final Collection<String> paths) {
        String[] arr = paths.toArray(new String[paths.size()]);
        if (inScan.compareAndSet(false, true)) {
            m_scanTask = new ScanTask(SettingsManager.getAppSettings().getAllowedFileTypes());
            m_scanTask.execute(arr);
        } else {
            m_scanTask.addPaths(arr);
        }
    }

    public void stopScan() {
        if (inScan.compareAndSet(true, false)) {
            m_scanTask = null;
        }
    }

    public FileObserver getObserver(final File dir) {
        final String path = dir.getAbsolutePath();
        synchronized (observers) {
            FileObserver fo = observers.get(path);
            if (fo == null) {
                fo = new FileObserverImpl(dir);
                observers.put(dir, fo);
            }
            return fo;
        }
    }

    public void removeObserver(final File dir) {
        synchronized (observers) {
            observers.remove(dir);
        }
    }

    public void stopObservers() {
        synchronized (observers) {
            for (final FileObserver o : observers.values()) {
                o.stopWatching();
            }
            observers.clear();
        }
    }

    class ScanTask extends AsyncTask<String, String, Void> {

        final FileExtensionFilter filter;

        final LinkedList<File> paths = new LinkedList<File>();

        public ScanTask(final FileExtensionFilter filter) {
            this.filter = filter;
        }

        @Override
        protected void onPreExecute() {
            base.showProgress(true);
        }

        @Override
        protected Void doInBackground(final String... paths) {
            addPaths(paths);

            for(File dir = getDir(); dir != null && inScan.get(); dir = getDir()) {
                scanDir(dir);
            }
            return null;
        }

        @Override
        protected void onPostExecute(final Void v) {
            base.showProgress(false);
            inScan.set(false);
        }

        void scanDir(final File dir) {
            // Checks if scan should be continued
            if (!inScan.get() || !dir.isDirectory() || dir.getAbsolutePath().startsWith("/sys")) {
                return;
            }

            // Retrieves file observer for scanning folder
            final FileObserver observer = getObserver(dir);
            // Stop watching
            observer.stopWatching();

            // Retrieves listener
            final Listener l = listeners.getListener();

            // Retrieves file list
            final File[] files = dir.listFiles((FilenameFilter) filter);
            // Sort file list
            if (LengthUtils.isNotEmpty(files)) {
                Arrays.sort(files, StringUtils.NFC);
            }
            // Call the file scan callback
            l.onFileScan(dir, files);

            // Retrieves files from current directory
            final File[] childDirs = dir.listFiles(DirectoryFilter.ALL);
            // Immediately starts folder watching
            getObserver(dir).startWatching();

            if (LengthUtils.isNotEmpty(childDirs)) {
                // Sort child dir list
                Arrays.sort(childDirs, StringUtils.NFC);
                // Add children for deep ordered scanning
                synchronized (this) {
                    for (int i = childDirs.length - 1; i >= 0; i--) {
                        this.paths.addFirst(childDirs[i]);
                    }
                }
            }
        }

        synchronized void addPaths(final String... paths) {
            for (String path : paths) {
                final File dir = new File(path);
                if (dir.exists() && dir.isDirectory()) {
                    this.paths.add(dir);
                }
            }
        }

        synchronized File getDir() {
            return this.paths.isEmpty() ? null : this.paths.removeFirst();
        }
    }

    public class FileObserverImpl extends FileObserver {

        private final File folder;

        public FileObserverImpl(File folder) {
            super(folder.getAbsolutePath(), EVENT_MASK);
            this.folder = folder;
        }

        @Override
        public void onEvent(final int event, final String path) {
            if (folder == null || path == null) {
                return;
            }

            final File f = new File(folder, path);
            final boolean isDirectory = f.isDirectory();
            final Listener l = listeners.getListener();

            switch (event) {
                case CREATE:
                case MOVED_TO:
                    if (isDirectory) {
                        l.onDirAdded(folder, f);
                        getObserver(f).startWatching();
                    } else {
                        l.onFileAdded(folder, f);
                    }
                    break;
                case DELETE:
                case MOVED_FROM:
                    if (isDirectory) {
                        l.onDirDeleted(folder, f);
                        removeObserver(f);
                    } else {
                        l.onFileDeleted(folder, f);
                    }
                    break;
                default:
                    break;
            }
        }

    }

    public static interface Listener {

        void onFileScan(File parent, File[] files);

        void onFileAdded(File parent, File f);

        void onFileDeleted(File parent, File f);

        void onDirAdded(File parent, File f);

        void onDirDeleted(File parent, File f);
    }
}